From fd6198e2af1c9b975f28a2e24ce06ef56f03cda7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20S=C3=A1nchez-Gallego?= Date: Wed, 22 Nov 2023 18:27:51 -0800 Subject: [PATCH] Removed most code --- poetry.lock | 78 ++-- pyproject.toml | 6 +- src/lvmnps/__init__.py | 5 +- src/lvmnps/__main__.py | 36 +- src/lvmnps/actor/actor.py | 137 ++----- src/lvmnps/actor/commands/__init__.py | 57 +-- src/lvmnps/actor/commands/onoff.py | 167 -------- src/lvmnps/actor/commands/outlets.py | 45 --- src/lvmnps/actor/commands/status.py | 58 +-- src/lvmnps/actor/commands/switches.py | 25 -- src/lvmnps/{etc => actor}/schema.json | 0 src/lvmnps/etc/lvmnps.yml | 42 --- src/lvmnps/etc/lvmnps_dummy.yml | 60 --- src/lvmnps/etc/lvmnps_netio.yml | 24 -- src/lvmnps/etc/lvmnps_pwi.yml | 32 -- src/lvmnps/etc/lvmnps_telescope.yml | 79 ---- src/lvmnps/exceptions.py | 31 -- src/lvmnps/switch/dli/__init__.py | 0 src/lvmnps/switch/dli/dli.py | 216 ----------- src/lvmnps/switch/dli/powerswitch.py | 163 -------- src/lvmnps/switch/dummy/__init__.py | 0 src/lvmnps/switch/dummy/powerswitch.py | 51 --- src/lvmnps/switch/factory.py | 63 ---- src/lvmnps/switch/iboot/iboot.py | 344 ----------------- src/lvmnps/switch/iboot/powerswitch.py | 76 ---- src/lvmnps/switch/netio/Netio/Device.py | 215 ----------- src/lvmnps/switch/netio/Netio/__init__.py | 1 - src/lvmnps/switch/netio/Netio/__main__.py | 5 - src/lvmnps/switch/netio/Netio/cli.py | 397 -------------------- src/lvmnps/switch/netio/Netio/exceptions.py | 14 - src/lvmnps/switch/netio/__init__.py | 6 - src/lvmnps/switch/netio/powerswitch.py | 90 ----- src/lvmnps/switch/outlet.py | 96 ----- src/lvmnps/switch/powerswitchbase.py | 269 ------------- tests/config.yaml | 5 + tests/conftest.py | 48 --- tests/test_actor.py | 62 --- tests/test_async.py | 47 --- tests/test_dli.py | 131 ------- tests/test_dli_switch.yml | 15 - tests/test_onoff.py | 165 -------- tests/test_outlets.py | 44 --- tests/test_status.py | 71 ---- tests/test_switch.yml | 51 --- tests/test_switches.py | 29 -- 45 files changed, 92 insertions(+), 3464 deletions(-) delete mode 100644 src/lvmnps/actor/commands/onoff.py delete mode 100644 src/lvmnps/actor/commands/outlets.py delete mode 100644 src/lvmnps/actor/commands/switches.py rename src/lvmnps/{etc => actor}/schema.json (100%) delete mode 100644 src/lvmnps/etc/lvmnps.yml delete mode 100644 src/lvmnps/etc/lvmnps_dummy.yml delete mode 100644 src/lvmnps/etc/lvmnps_netio.yml delete mode 100644 src/lvmnps/etc/lvmnps_pwi.yml delete mode 100644 src/lvmnps/etc/lvmnps_telescope.yml delete mode 100644 src/lvmnps/exceptions.py delete mode 100644 src/lvmnps/switch/dli/__init__.py delete mode 100644 src/lvmnps/switch/dli/dli.py delete mode 100644 src/lvmnps/switch/dli/powerswitch.py delete mode 100644 src/lvmnps/switch/dummy/__init__.py delete mode 100644 src/lvmnps/switch/dummy/powerswitch.py delete mode 100644 src/lvmnps/switch/factory.py delete mode 100644 src/lvmnps/switch/iboot/iboot.py delete mode 100644 src/lvmnps/switch/iboot/powerswitch.py delete mode 100644 src/lvmnps/switch/netio/Netio/Device.py delete mode 100644 src/lvmnps/switch/netio/Netio/__init__.py delete mode 100644 src/lvmnps/switch/netio/Netio/__main__.py delete mode 100644 src/lvmnps/switch/netio/Netio/cli.py delete mode 100644 src/lvmnps/switch/netio/Netio/exceptions.py delete mode 100644 src/lvmnps/switch/netio/__init__.py delete mode 100644 src/lvmnps/switch/netio/powerswitch.py delete mode 100644 src/lvmnps/switch/outlet.py delete mode 100644 src/lvmnps/switch/powerswitchbase.py create mode 100644 tests/config.yaml delete mode 100644 tests/conftest.py delete mode 100644 tests/test_actor.py delete mode 100644 tests/test_async.py delete mode 100644 tests/test_dli.py delete mode 100644 tests/test_dli_switch.yml delete mode 100644 tests/test_onoff.py delete mode 100644 tests/test_outlets.py delete mode 100644 tests/test_status.py delete mode 100644 tests/test_switch.yml delete mode 100644 tests/test_switches.py diff --git a/poetry.lock b/poetry.lock index 5a79cd7..65ea4ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -57,24 +57,24 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} [[package]] name = "anyio" -version = "3.7.1" +version = "4.1.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"}, + {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "appnope" @@ -609,54 +609,59 @@ sphinx-basic-ng = "*" [[package]] name = "h11" -version = "0.12.0" +version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] [[package]] name = "httpcore" -version = "0.13.7" +version = "1.0.2" description = "A minimal low-level HTTP client." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, - {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, ] [package.dependencies] -anyio = "==3.*" -h11 = ">=0.11,<0.13" -sniffio = "==1.*" +certifi = "*" +h11 = ">=0.13,<0.15" [package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] [[package]] name = "httpx" -version = "0.18.2" +version = "0.25.1" description = "The next generation HTTP client." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"}, - {file = "httpx-0.18.2.tar.gz", hash = "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"}, + {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"}, + {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"}, ] [package.dependencies] +anyio = "*" certifi = "*" -httpcore = ">=0.13.3,<0.14.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +httpcore = "*" +idna = "*" sniffio = "*" [package.extras] -brotli = ["brotlicffi (==1.*)"] -http2 = ["h2 (==3.*)"] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] [[package]] name = "idna" @@ -1782,23 +1787,6 @@ files = [ [package.dependencies] docutils = ">=0.11,<1.0" -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - [[package]] name = "rich" version = "13.7.0" @@ -2640,4 +2628,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "d17f8ed0b2852a7a0f8d88903d47b3eeeaa835d920fb1980542a2339dc15add9" +content-hash = "85b973a9cc59be1335b33c5793e697d9312db6fa78970fe74ff6f53666489e79" diff --git a/pyproject.toml b/pyproject.toml index 4f4c75c..b52efba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ packages = [ { include = "lvmnps", from = "src" } ] -include = ["python/lvmnps/etc/*"] +include = ["src/lvmnps/actor/schema.json"] [tool.poetry.scripts] lvmnps = "lvmnps.__main__:main" @@ -34,7 +34,7 @@ python = ">=3.8,<4.0" sdsstools = "^1.0.0" click-default-group = "^1.2.2" sdss-clu = "^2.0.0" -httpx = "^0.18.1" +httpx = ">=0.18.1" [tool.poetry.group.dev.dependencies] ipython = ">=7.11.0" @@ -92,7 +92,7 @@ asyncio_mode = "auto" branch = true disable_warnings = ["include-ignored"] omit = [ - "python/lvmnps/__main__.py", + "src/lvmnps/__main__.py", ] [tool.coverage.report] diff --git a/src/lvmnps/__init__.py b/src/lvmnps/__init__.py index d032a79..cb7250b 100644 --- a/src/lvmnps/__init__.py +++ b/src/lvmnps/__init__.py @@ -9,12 +9,9 @@ from sdsstools import get_config, get_logger, get_package_version -# pip package name NAME = "sdss-lvmnps" -# Loads config. config name is the package name. -config = get_config("lvmnps") + log = get_logger(NAME) -# package name should be pip package name __version__ = get_package_version(path=__file__, package_name=NAME) diff --git a/src/lvmnps/__main__.py b/src/lvmnps/__main__.py index 956eed4..35c87e6 100644 --- a/src/lvmnps/__main__.py +++ b/src/lvmnps/__main__.py @@ -29,38 +29,20 @@ def wrapper(*args, **kwargs): "--config", "config_file", type=click.Path(exists=True, dir_okay=False), + required=True, help="Path to the user configuration file.", ) -@click.option( - "-r", - "--rmq_url", - "rmq_url", - default=None, - type=str, - help="rabbitmq url, eg: amqp://guest:guest@localhost:5672/", -) @click.option( "-v", "--verbose", count=True, help="Debug mode. Use additional v for more details.", ) -@click.option( - "-s", - "--simulate", - count=True, - help="Simulation mode. Overwrite configured nps device with a dummy device", -) @click.pass_context -def lvmnps(ctx, config_file, rmq_url, verbose, simulate): - """Nps Actor.""" +def lvmnps(ctx: click.Context, config_file: str, verbose: bool = False): + """Network Power Supply actor.""" - ctx.obj = { - "verbose": verbose, - "config_file": config_file, - "rmq_url": rmq_url, - "simulate": simulate, - } + ctx.obj = {"verbose": verbose, "config_file": config_file} @lvmnps.group(cls=DaemonGroup, prog="nps_actor", workdir=os.getcwd()) @@ -69,15 +51,9 @@ def lvmnps(ctx, config_file, rmq_url, verbose, simulate): async def actor(ctx): """Runs the actor.""" - default_config_file = os.path.join(os.path.dirname(__file__), "etc/lvmnps.yml") - config_file = ctx.obj["config_file"] or default_config_file + config_file = ctx.obj["config_file"] - lvmnps = NPSActor.from_config( - config_file, - url=ctx.obj["rmq_url"], - verbose=ctx.obj["verbose"], - simulate=ctx.obj["simulate"], - ) + lvmnps = NPSActor.from_config(config_file, verbose=ctx.obj["verbose"]) if ctx.obj["verbose"]: if lvmnps.log.fh: diff --git a/src/lvmnps/actor/actor.py b/src/lvmnps/actor/actor.py index 3ff7a65..0584100 100644 --- a/src/lvmnps/actor/actor.py +++ b/src/lvmnps/actor/actor.py @@ -8,136 +8,49 @@ from __future__ import annotations -import asyncio -import os +import pathlib +from os import PathLike -from typing import ClassVar, Dict - -import click +from typing import TYPE_CHECKING from clu import Command from clu.actor import AMQPActor -from lvmnps.actor.commands import parser as nps_command_parser -from lvmnps.switch.factory import powerSwitchFactory - - -__all__ = ["NPSActor"] - - -class NPSActor(AMQPActor): - """LVM network power switches base actor. - - Subclassed from the `.AMQPActor` class. - - """ - - parser: ClassVar[click.Group] = nps_command_parser - BASE_CONFIG: ClassVar[str | Dict | None] = None - - def __init__(self, *args, **kwargs): - if "schema" not in kwargs: - kwargs["schema"] = os.path.join( - os.path.dirname(__file__), - "../etc/schema.json", - ) - - super().__init__(*args, **kwargs) - - self.connect_timeout = 3 +from lvmnps import __version__ +from lvmnps import log as nps_log +from lvmnps.actor.commands import lvmnps_command_parser - async def start(self): - """Start the actor and connect the power switches.""" - await super().start() +if TYPE_CHECKING: + from sdsstools.logger import SDSSLogger - if "timeouts" in self.config and "switch_connect" in self.config["timeouts"]: - self.connect_timeout = self.config["timeouts"]["switch_connect"] - assert len(self.parser_args) == 1 - - switches = list(self.parser_args[0].values()) - - # self.parser_args[0] is the list of switch instances - - tasks = [ - asyncio.wait_for(switch.start(), timeout=self.connect_timeout) - for switch in switches - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - - valid_switches = [] - for ii, result in enumerate(results): - switch_name = switches[ii].name - if isinstance(result, Exception): - self.log.error( - f"Unexpected exception of type {type(result)} while initialising " - f"switch {switch_name}: {str(result)}" - ) - else: - valid_switches.append(switches[ii]) - - self.parser_args[0] = {switch.name: switch for switch in valid_switches} - - all_names = [ - outlet.name.lower() - for switch in valid_switches - for outlet in switch.outlets - ] - - if len(all_names) != len(list(set(all_names))): - self.log.warning("Repeated outlet names. This may lead to errors.") - - self.log.debug("Start done") - - async def stop(self): - """Stop the actor and disconnect the power switches.""" - - for switch in self.parser_args[0].values(): - try: - self.log.debug(f"Stop {switch.name} ...") - await asyncio.wait_for(switch.stop(), timeout=self.connect_timeout) - except Exception as ex: - self.log.error(f"Unexpected exception of {type(ex)}: {ex}") - - return await super().stop() +__all__ = ["NPSActor"] - @classmethod - def from_config(cls, config, *args, simulate: bool = False, **kwargs): - """Creates an actor from a configuration file.""" - if config is None: - if cls.BASE_CONFIG is None: - raise RuntimeError("The class does not have a base configuration.") - config = cls.BASE_CONFIG +AnyPath = str | PathLike[str] - instance = super(NPSActor, cls).from_config(config, *args, **kwargs) - assert isinstance(instance, NPSActor) - assert isinstance(instance.config, dict) +class NPSActor(AMQPActor): + """LVM network power switches base actor.""" - switches = {} + parser = lvmnps_command_parser - if "switches" in instance.config: - if simulate: - instance.log.warn("In simulation mode !!!") - for key, swconfig in instance.config["switches"].items(): - if "name" in swconfig: - name = swconfig["name"] - else: - name = key + def __init__( + self, + *args, + schema: AnyPath | None = None, + log: SDSSLogger | None = None, + **kwargs, + ): + cwd = pathlib.Path(__file__).parent - instance.log.info(f"Instance {name}: {swconfig}") - try: - switches[name] = powerSwitchFactory( - name, swconfig, instance.log, simulate=simulate - ) - except Exception as ex: - instance.log.error(f"Power switch factory error {type(ex)}: {ex}") + schema = schema or cwd / "schema.json" + log = log or nps_log - instance.parser_args = [switches] + kwargs["version"] = __version__ - return instance + super().__init__(*args, log=log, schema=schema, **kwargs) NPSCommand = Command[NPSActor] diff --git a/src/lvmnps/actor/commands/__init__.py b/src/lvmnps/actor/commands/__init__.py index ada461d..1d2173f 100644 --- a/src/lvmnps/actor/commands/__init__.py +++ b/src/lvmnps/actor/commands/__init__.py @@ -1,50 +1,13 @@ -import glob -import importlib -import os +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# @Author: José Sánchez-Gallego (gallegoj@uw.edu) +# @Date: 2023-11-22 +# @Filename: __init__.py +# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) -import click +from __future__ import annotations -from clu.command import Command -from clu.parsers.click import CluGroup, get_command_model, help_, ping, version +from clu.parsers.click import command_parser as lvmnps_command_parser - -@click.group(cls=CluGroup) -def parser(*args): - pass - - -parser.add_command(ping) -parser.add_command(version) -parser.add_command(help_) -parser.add_command(get_command_model) - - -@parser.command(name="__commands") -@click.pass_context -def __commands(ctx, command: Command, *args): - # Returns all commands. - - # we have to use the help key for the command list, - # don't want to change the standard model. - command.finish(help=[k for k in ctx.command.commands.keys() if k[:2] != "__"]) - - -parser.add_command(__commands) - - -# Autoimport all modules in this directory so that they are added to the parser. - -exclusions = ["__init__.py"] - -cwd = os.getcwd() -os.chdir(os.path.dirname(os.path.realpath(__file__))) - -files = [ - file_ for file_ in glob.glob("**/*.py", recursive=True) if file_ not in exclusions -] - -for file_ in files: - modname = file_[0:-3].replace("/", ".") - mod = importlib.import_module("lvmnps.actor.commands." + modname) - -os.chdir(cwd) +from .status import status diff --git a/src/lvmnps/actor/commands/onoff.py b/src/lvmnps/actor/commands/onoff.py deleted file mode 100644 index fe0ccd3..0000000 --- a/src/lvmnps/actor/commands/onoff.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2021-08-12 -# @Filename: onoff.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -import asyncio - -from typing import TYPE_CHECKING - -import click - -from lvmnps.actor.commands import parser - - -if TYPE_CHECKING: - from lvmnps.actor.actor import NPSCommand - from lvmnps.switch.powerswitchbase import PowerSwitchBase - - -async def switch_control( - command: str, - switch: PowerSwitchBase, - on: bool, - name: str, - portnum: int | None, -): - """The function for parsing the actor command to the switch library.""" - - status = {} - if command == "on" or command == "off": - await switch.setState(on, name, portnum) - # status |= await switch.statusAsDict(name, portnum) works only with python 3.9 - status[switch.name] = dict( - list(status.items()) - + list((await switch.statusAsDict(name, portnum)).items()) # noqa: W503 - ) - - return status - - -@parser.command() -@click.argument("OUTLET", type=str) -@click.argument("PORTNUM", type=int, required=False) -@click.option( - "--switch", - type=str, - help="Address this switch specifically. Otherwise the first switch " - "with an outlet that matches NAME will be commanded.", -) -@click.option("--off-after", type=float, help="Turn off after X seconds.") -async def on( - command: NPSCommand, - switches: dict[str, PowerSwitchBase], - outlet: str, - portnum: int | None = None, - switch: str | None = None, - off_after: float | None = None, -): - """ - Turn on the outlet. - - \b - :param OUTLET: Outlet or switch name. - :param PORTNUM: Portnumber if switch name is provided. - """ - - if portnum: - command.info(text=f"Turning on {outlet} port {portnum} ...") - else: - command.info(text=f"Turning on outlet {outlet} ...") - - the_switch: PowerSwitchBase | None = None - current_status: dict | None = None - for sw in switches.values(): - if switch and sw.name != switch: - continue - - current_status = await sw.statusAsDict(outlet, portnum) - if current_status: - the_switch = sw - break - - if current_status is None or the_switch is None: - return command.fail(f"Could not find a match for {outlet}:{portnum}.") - - outletname_list = list(current_status.keys()) - outletname = outletname_list[0] - - if current_status[outletname]["state"] != 1: - current_status = await switch_control("on", the_switch, True, outlet, portnum) - elif current_status[outletname]["state"] == 1: - return command.finish(text=f"The outlet {outletname} is already ON") - else: - return command.fail(text=f"The outlet {outletname} returns wrong value") - - command.info(status=current_status) - - if off_after is not None: - command.info(f"The switch will be turned off after {off_after} seconds.") - await asyncio.sleep(off_after - 1) - current_status = await switch_control("off", the_switch, False, outlet, portnum) - command.info(status=current_status) - - return command.finish() - - -@parser.command() -@click.argument("OUTLET", type=str) -@click.argument("PORTNUM", type=int, required=False) -@click.option( - "--switch", - type=str, - help="Address this switch specifically. Otherwise the first switch " - "with an outlet that matches NAME will be commanded.", -) -async def off( - command: NPSCommand, - switches: dict[str, PowerSwitchBase], - outlet: str, - portnum: int | None = None, - switch: str | None = None, -): - """ - Turn off the outlet. - - \b - :param OUTLET: Outlet or switch name. - :param PORTNUM: Portnumber if switch name is provided. - """ - - if portnum: - command.info(text=f"Turning off {outlet} port {portnum} ...") - else: - command.info(text=f"Turning off outlet {outlet} ...") - - the_switch: PowerSwitchBase | None = None - current_status: dict | None = None - for sw in switches.values(): - if switch and sw.name != switch: - continue - - current_status = await sw.statusAsDict(outlet, portnum) - if current_status: - the_switch = sw - break - - if current_status is None or the_switch is None: - return command.fail(f"Could not find a match for {outlet}:{portnum}.") - - outletname_list = list(current_status.keys()) - outletname = outletname_list[0] - - if current_status[outletname]["state"] != 0: - current_status = await switch_control("off", the_switch, False, outlet, portnum) - elif current_status[outletname]["state"] == 0: - return command.finish(text=f"The outlet {outletname} is already OFF") - else: - return command.fail(text=f"The outlet {outletname} returns wrong value") - - command.info(status=current_status) - - return command.finish() diff --git a/src/lvmnps/actor/commands/outlets.py b/src/lvmnps/actor/commands/outlets.py deleted file mode 100644 index f675e5c..0000000 --- a/src/lvmnps/actor/commands/outlets.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# @Author: José Sánchez-Gallego (gallegoj@uw.edu) -# @Date: 2022-05-22 -# @Filename: outlets.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import click - -from lvmnps.actor.commands import parser - - -if TYPE_CHECKING: - from lvmnps.actor.actor import NPSCommand - from lvmnps.switch.powerswitchbase import PowerSwitchBase - - -@parser.command() -@click.argument("SWITCHNAME", type=str, required=False) -async def outlets( - command: NPSCommand, - switches: dict[str, PowerSwitchBase], - switchname: str | None = None, -): - """Returns the list of names for each outlet on a specific power switch.""" - - if switchname: - if switchname not in switches: - return command.fail(f"Unknown switch {switchname}.") - switch_instances = [switches[switchname]] - else: - switch_instances = list(switches.values()) - - outlets = [] - for switch in switch_instances: - for o in switch.outlets: - if o.inuse or not switch.onlyusedones: - outlets.append(o.name) - - return command.finish(outlets=outlets) diff --git a/src/lvmnps/actor/commands/status.py b/src/lvmnps/actor/commands/status.py index de7af0f..678c5e4 100644 --- a/src/lvmnps/actor/commands/status.py +++ b/src/lvmnps/actor/commands/status.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2021-08-12 +# @Author: José Sánchez-Gallego (gallegoj@uw.edu) +# @Date: 2023-11-22 # @Filename: status.py # @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) @@ -10,58 +10,18 @@ from typing import TYPE_CHECKING -import click - -from lvmnps.actor.commands import parser +from . import lvmnps_command_parser if TYPE_CHECKING: - from lvmnps.actor.actor import NPSCommand - from lvmnps.switch.powerswitchbase import PowerSwitchBase - + from src.lvmnps.actor.actor import NPSCommand -@parser.command() -@click.argument("SWITCHNAME", type=str, required=False) -@click.argument("PORTNUM", type=int, required=False) -@click.option( - "-o", - "--outlet", - type=str, - help="Print only the information for this outlet.", -) -async def status( - command: NPSCommand, - switches: dict[str, PowerSwitchBase], - switchname: str | None = None, - portnum: int | None = None, - outlet: str | None = None, -): - """ - Returns the dictionary of a specific outlet. - \b - :param SWITCHNAME: Switch name. - :param PORTNUM: Portnumber. - """ - if switchname and switchname not in switches: - return command.fail(f"Unknown switch {switchname}.") +__all__ = ["status"] - status = {} - if switchname is None: - for switch in switches.values(): - if not await switch.isReachable(): - continue - current_status = await switch.statusAsDict(outlet, portnum) - if current_status: - status[switch.name] = current_status - else: - switch = switches[switchname] - if await switch.isReachable(): - current_status = await switch.statusAsDict(outlet, portnum) - if current_status: - status[switch.name] = current_status - if status == {}: - return command.fail("Unable to find matching outlets.") +@lvmnps_command_parser.command() +async def status(command: NPSCommand): + """Returns the status of the network power switch.""" - return command.finish(message={"status": status}) + command.finish() diff --git a/src/lvmnps/actor/commands/switches.py b/src/lvmnps/actor/commands/switches.py deleted file mode 100644 index 85d36cb..0000000 --- a/src/lvmnps/actor/commands/switches.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# @Author: José Sánchez-Gallego (gallegoj@uw.edu) -# @Date: 2022-05-22 -# @Filename: switches.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from . import parser - - -if TYPE_CHECKING: - from lvmnps.actor.actor import NPSCommand - from lvmnps.switch.powerswitchbase import PowerSwitchBase - - -@parser.command() -async def switches(command: NPSCommand, switches: dict[str, PowerSwitchBase]): - """Lists the available switches.""" - - return command.finish(switches=list(switches.keys())) diff --git a/src/lvmnps/etc/schema.json b/src/lvmnps/actor/schema.json similarity index 100% rename from src/lvmnps/etc/schema.json rename to src/lvmnps/actor/schema.json diff --git a/src/lvmnps/etc/lvmnps.yml b/src/lvmnps/etc/lvmnps.yml deleted file mode 100644 index e2f7873..0000000 --- a/src/lvmnps/etc/lvmnps.yml +++ /dev/null @@ -1,42 +0,0 @@ -# A dictionary of controller name to NPS controller connection parameters. -switches: - DLI-01: - type: dli - name: DLI1 - hostname: 10.7.45.22 - user: 'admin' - password: 'rLXR3KxUqiCPGvA' - onoff_timeout: 3 - ouo: False # handle also unconfigured ports - ports: - number_of_ports: 8 - DLI-02: - type: dli - name: DLI2 - hostname: 10.7.45.29 - user: 'admin' - password: 'VCrht9wfx2CQN9b' - onoff_timeout: 3 - ouo: False # handle also unconfigured ports - ports: - number_of_ports: 8 - DLI-03: - type: dli - name: DLI3 - hostname: 10.7.45.31 - user: 'admin' - password: 'JNC_zbf5tdc4deb*npx' - onoff_timeout: 3 - ouo: False # handle also unconfigured ports - ports: - number_of_ports: 8 - -timeouts: - switch_connect: 3 - -# Actor configuration for the AMQPActor class -actor: - name: lvmnps - host: localhost - port: 5672 - log_dir: '/data/logs/lvmnps' diff --git a/src/lvmnps/etc/lvmnps_dummy.yml b/src/lvmnps/etc/lvmnps_dummy.yml deleted file mode 100644 index 5c9b25d..0000000 --- a/src/lvmnps/etc/lvmnps_dummy.yml +++ /dev/null @@ -1,60 +0,0 @@ -# A dictionary of controller name to NPS controller connection parameters. -switches: - cab: - type: dummy - num: 4 - ports: - 1: - name: 'cab.mocon' - desc: 'Mocon motor controller' - sci: - type: dummy - num: 4 - ports: - 1: - name: 'sci.pwi' - desc: 'Planewave mount' - 2: - name: 'sci.tpc' - desc: 'Planewave tiny pc' - skye: - type: dummy - num: 4 - ports: - 1: - name: 'skye.pwi' - desc: 'Planewave mount' - 2: - name: 'skye.tpc' - desc: 'Planewave tiny pc' - skyw: - type: dummy - num: 4 - ports: - 1: - name: 'skyw.pwi' - desc: 'Planewave mount' - 2: - name: 'skyw.tpc' - desc: 'Planewave tiny pc' - spec: - type: dummy - num: 4 - ports: - 1: - name: 'spec.pwi' - desc: 'Planewave mount' - 2: - name: 'spec.tpc' - desc: 'Planewave tiny pc' - - -timeouts: - switch_connect: 1 - -# Actor configuration for the AMQPActor class -actor: - name: lvmnps - host: localhost - port: 5672 - log_dir: '~/tmp/log' diff --git a/src/lvmnps/etc/lvmnps_netio.yml b/src/lvmnps/etc/lvmnps_netio.yml deleted file mode 100644 index b5a578e..0000000 --- a/src/lvmnps/etc/lvmnps_netio.yml +++ /dev/null @@ -1,24 +0,0 @@ -# A dictionary of controller name to NPS controller connection parameters. -switches: - skye.nps: - type: netio -# hostname: '192.168.1.17' - hostname: 'localhost:8080' - username: 'netio' - password: 'netio' - ouo: False - ports: - number_of_ports: 4 - 1: - name: 'skye.pwi' - desc: 'PlaneWavemount Skye' - -timeouts: - switch_connect: 1 - -# Actor configuration for the AMQPActor class -actor: - name: lvmnps - host: localhost - port: 5672 - log_dir: '~/tmp/log' diff --git a/src/lvmnps/etc/lvmnps_pwi.yml b/src/lvmnps/etc/lvmnps_pwi.yml deleted file mode 100644 index 19ae8d6..0000000 --- a/src/lvmnps/etc/lvmnps_pwi.yml +++ /dev/null @@ -1,32 +0,0 @@ -# A dictionary of controller name to NPS controller connection parameters. -switches: - skye.nps: - type: iboot - hostname: '192.168.178.52' - username: 'admin' - password: 'admin' - ports: - num: 1 - 1: - name: 'skye.pwi' - desc: 'PlaneWavemount Skye' - skyw.nps: - type: iboot - hostname: '192.168.178.53' - username: 'admin' - password: 'admin' - ports: - num: 1 - 1: - name: 'skyw.pwi' - desc: 'PlaneWavemount Skye' - -timeouts: - switch_connect: 1 - -# Actor configuration for the AMQPActor class -actor: - name: lvmnps - host: localhost - port: 5672 - log_dir: '~/tmp/log' diff --git a/src/lvmnps/etc/lvmnps_telescope.yml b/src/lvmnps/etc/lvmnps_telescope.yml deleted file mode 100644 index 4390e1a..0000000 --- a/src/lvmnps/etc/lvmnps_telescope.yml +++ /dev/null @@ -1,79 +0,0 @@ -# A dictionary of controller name to NPS controller connection parameters. -switches: - cab: - type: netio - hostname: '10.8.38.110:8080' - username: 'netio' - password: 'netio' - num: 4 - ports: - 1: - name: 'cab.mocon' - desc: 'Mocon motor controller' - - sci: - type: netio - hostname: '10.8.38.106:8080' - username: 'netio' - password: 'netio' - num: 4 - ports: - 1: - name: 'sci.pwi' - desc: 'Planewave mount' - 2: - name: 'sci.tpc' - desc: 'Planewave tiny pc' - - skye: - type: netio - hostname: '10.8.38.107:8080' - username: 'netio' - password: 'netio' - num: 4 - ports: - 1: - name: 'skye.pwi' - desc: 'Planewave mount' - 2: - name: 'skye.tpc' - desc: 'Planewave tiny pc' - - skyw: - type: netio - hostname: '10.8.38.108:8080' - username: 'netio' - password: 'netio' - num: 4 - ports: - 1: - name: 'skyw.pwi' - desc: 'Planewave mount' - 2: - name: 'skyw.tpc' - desc: 'Planewave tiny pc' - - spec: - type: netio - hostname: '10.8.38.109:8080' - username: 'netio' - password: 'netio' - num: 4 - ports: - 1: - name: 'spec.pwi' - desc: 'Planewave mount' - 2: - name: 'spec.tpc' - desc: 'Planewave tiny pc' - - -timeouts: - switch_connect: 1 - -# Actor configuration for the AMQPActor class -actor: - name: lvmnps - host: localhost - port: 5672 - log_dir: '~/tmp/log' diff --git a/src/lvmnps/exceptions.py b/src/lvmnps/exceptions.py deleted file mode 100644 index 0d92c7a..0000000 --- a/src/lvmnps/exceptions.py +++ /dev/null @@ -1,31 +0,0 @@ -# !usr/bin/env python -# -*- coding: utf-8 -*- -# -# Licensed under a 3-clause BSD license. -# -# @Author: Mingyeong Yang (mingyeong@khu.ac.kr), Changgon Kim (changgonkim@khu.ac.kr) -# @Date: 2021-08-24 -# @Update: 2021-10-09 -# @Filename: lvmnps/switch/dli/dli.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import absolute_import, division, print_function - - -class NpsActorError(Exception): - """A custom core NpsActor exception""" - - def __init__(self, message=None): - message = "There has been an error" if not message else message - - super(NpsActorError, self).__init__(message) - - -class NpsActorWarning(Warning): - """Base warning for NpsActor.""" - - -class PowerException(Exception): - """An error Exception class for powerswitch factory.""" - - pass diff --git a/src/lvmnps/switch/dli/__init__.py b/src/lvmnps/switch/dli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/lvmnps/switch/dli/dli.py b/src/lvmnps/switch/dli/dli.py deleted file mode 100644 index 5665bc4..0000000 --- a/src/lvmnps/switch/dli/dli.py +++ /dev/null @@ -1,216 +0,0 @@ -# !usr/bin/env python -# -*- coding: utf-8 -*- -# -# @Author: Mingyeong Yang (mingyeong@khu.ac.kr), Changgon Kim (changgonkim@khu.ac.kr) -# @Date: 2021-08-24 -# @Update: 2021-10-09 -# @Filename: lvmnps/switch/dli/dli.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -import asyncio -import logging - -from typing import TYPE_CHECKING - -import httpx - - -if TYPE_CHECKING: - from sdsstools.logger import SDSSLogger - - from ..outlet import Outlet - - -class DLI(object): - """Powerswitch class to manage the DLI power switch. - - Parameters - ---------- - hostname - The hostname from the configuration (the IP address for connection). - user - The username from the configuration (the id for login). - password - The password from the configuration (the password for login). - name - The name of the DLI Controller. - log - The logger for logging. - onoff_timeout - The timeout, in seconds, before failing an on/off command. - - """ - - def __init__( - self, - hostname: str, - user: str, - password: str, - name: str | None = None, - log: SDSSLogger | None = None, - onoff_timeout=3, - ): - self.user = user - self.hostname = hostname - self.name = name or hostname - - self.log = log or logging.getLogger(f"{self.__class__.__name__}_{self.name}") - - self.onoff_timeout = onoff_timeout - - self.client: httpx.AsyncClient - self.add_client(password) - - self.lock = asyncio.Lock() - - def add_client(self, password: str): - """Add the `httpx.AsyncClient` to the DLI object.""" - - try: - auth = httpx.DigestAuth(self.user, password) - self.client = httpx.AsyncClient( - auth=auth, - base_url=f"http://{self.hostname}/restapi", - headers={}, - ) - except Exception as ex: - self.log.error(f"{type(ex)}: couldn't access client {self.hostname}: {ex}") - - async def verify(self, outlets: list[Outlet]): - """Verifies if we can reach the switch by the "get" method. - - Also compares the outlet lists with the configuration, and returns true - if it's identical. - - Parameters - ---------- - outlets - The list of `.Outlet` instance to check. - - """ - - result = False - - async with self.lock: - async with self.client as client: - r = await client.get("relay/outlets/") - if r.status_code != 200: - raise RuntimeError(f"GET returned code {r.status_code}.") - else: - result = self.compare(r.json(), outlets) - - return result - - def compare(self, json: dict, outlets: list[Outlet]): - """Compares the names of the outlets with the response JSON object. - - Parameters - ---------- - json - The json list from the restful API. The current status of the power - switch is contained here. - outlets - List of `.Outlet` objects to compare. - - """ - - same = True - - for outlet in outlets: - portnum = outlet.portnum - if json[portnum - 1]["name"] != outlet.name: - same = False - break - - return same - - async def on(self, outlet: int = 0): - """Turn on the power to the outlet. - - Set the value of the outlet state by using a PUT request. Note that the - outlets in the RESTful API are zero-indexed. - - Parameters - ---------- - outlet - The number indicating the outlet (1-indexed). - - """ - - outlet = outlet - 1 - - async with self.lock: - async with self.client as client: - r = await asyncio.wait_for( - client.put( - f"relay/outlets/{outlet}/state/", - data={"value": True}, - headers={"X-CSRF": "x"}, - ), - self.onoff_timeout, - ) - if r.status_code != 204: - raise RuntimeError(f"PUT returned code {r.status_code}.") - - async def off(self, outlet=0): - """Turn off the power to the outlet. - - Set the value of the outlet state by using a PUT request. Note that the - outlets in the RESTful API are zero-indexed. - - Parameters - ---------- - outlet - The number indicating the outlet (1-indexed). - - """ - - outlet = outlet - 1 - - async with self.lock: - async with self.client as client: - r = await asyncio.wait_for( - client.put( - f"relay/outlets/{outlet}/state/", - data={"value": False}, - headers={"X-CSRF": "x"}, - ), - self.onoff_timeout, - ) - if r.status_code != 204: - raise RuntimeError(f"PUT returned code {r.status_code}.") - - async def get_outlets_response(self): - """Returns the raw response to a ``relay/outlets`` GET request..""" - - async with self.lock: - async with self.client as client: - r = await client.get("relay/outlets/") - if r.status_code != 200: - raise RuntimeError(f"GET returned code {r.status_code}.") - - return r.json() - - async def status(self): - """Returns the status as a dictionary. - - Receives the data from the switch by the GET method as a JSON. Note that - this method returns the status of all the outlets (ports 1-8). - - """ - - async with self.lock: - async with self.client as client: - r = await client.get("relay/outlets/") - if r.status_code != 200: - raise RuntimeError(f"GET returned code {r.status_code}.") - - outlets_dict = {} - - data = r.json() - for n in range(0, 8): - outlets_dict[n + 1] = data[n]["state"] - - return outlets_dict diff --git a/src/lvmnps/switch/dli/powerswitch.py b/src/lvmnps/switch/dli/powerswitch.py deleted file mode 100644 index 380e6a7..0000000 --- a/src/lvmnps/switch/dli/powerswitch.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de), -# Mingyeong Yang (mingyeong@khu.ac.kr), -# Changgon Kim (changgonkim@khu.ac.kr) -# @Date: 2021-06-24 -# @Update: 2021-10-09 -# @Filename: lvmnps/switch/dli/powerswitch.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from lvmnps.switch.dli.dli import DLI -from lvmnps.switch.powerswitchbase import PowerSwitchBase - - -if TYPE_CHECKING: - from sdsstools.logger import SDSSLogger - - from ..outlet import Outlet - - -__all__ = ["DLIPowerSwitch"] - - -class DLIPowerSwitch(PowerSwitchBase): - """A DLI power switch. - - Parameters - ---------- - name - A name identifying the power switch. - config - The configuration defined on the .yaml file under ``/etc/lvmnps_dli.yml``. - log - The logger for logging. - - """ - - def __init__(self, name: str, config: dict, log: SDSSLogger | None = None): - super().__init__(name, config, log) - - hostname = self.config_get("hostname") - user = self.config_get("user") - password = self.config_get("password") - - onoff_timeout = self.config_get("onoff_timeout", 3) - - if hostname is None or user is None or password is None: - raise ValueError( - "Hostname or credentials are missing. " - "Cannot create new DLI instance." - ) - - self.name = name - - self.dli = DLI( - hostname, - user, - password, - log=self.log, - name=self.name, - onoff_timeout=onoff_timeout, - ) - - self.reachable = False - - async def start(self): - """Adds the client controlling the DLI Power Switch. - - Checks if the Power switch is reachable. - If the Power switch is reachable, updates the data of Outlet objects. - - """ - - # Instead of sending several requests, which makes it a bit slower, - # get all the information form the outlets and manually update states. - - outlet_data = await self.dli.get_outlets_response() - - for i in range(len(outlet_data)): - outlet = self.collectOutletsByNameAndPort(portnum=i + 1) - if len(outlet) == 0 or len(outlet) > 1: - continue - - outlet[0].setState(outlet_data[i]["state"]) - - if self.onlyusedones is False: - if outlet[0].inuse: - continue - - if outlet_data[i]["name"] == "": - continue - - outlet[0].name = outlet_data[i]["name"] - outlet[0].description = outlet_data[i]["name"] - outlet[0].inuse = True - - try: - inuse = [outlet for outlet in self.outlets if outlet.inuse] - self.reachable = await self.dli.verify(inuse) - except Exception as ex: - raise RuntimeError(f"Unexpected exception is {type(ex)}: {ex}") - - async def stop(self): - """Closes the connection to the client.""" - - pass - - async def isReachable(self): - """Check if the power switch is reachable.""" - - return self.reachable - - async def update(self, outlets: list[Outlet] | None = None): - """Updates the data based on the received status dictionary from the DLI class. - - Parameters - ---------- - outlets - List of `.Outlet` objects. If `None`, updates the status of all outlets. - - """ - - outlets = outlets or self.outlets - - try: - if self.reachable: - # set the status to the real state - status = await self.dli.status() - for o in outlets: - o.setState(status[o.portnum]) - else: - for o in outlets: - o.setState(-1) - except Exception as ex: - for o in outlets: - o.setState(-1) - raise RuntimeError(f"Unexpected exception for {type(ex)}: {ex}") - - async def switch(self, state: bool, outlets: list[Outlet]): - """Controls the switch (turning on or off). - - Parameters - ---------- - state - The state to which to switch the outlet(s). - outlets - List of outlets to command. - - """ - - state = bool(state) - - try: - if self.reachable: - for o in outlets: - await (self.dli.on(o.portnum) if state else self.dli.off(o.portnum)) - await self.update(outlets) - except Exception as ex: - raise RuntimeError(f"Unexpected exception to {type(ex)}: {ex}") diff --git a/src/lvmnps/switch/dummy/__init__.py b/src/lvmnps/switch/dummy/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/lvmnps/switch/dummy/powerswitch.py b/src/lvmnps/switch/dummy/powerswitch.py deleted file mode 100644 index 739de12..0000000 --- a/src/lvmnps/switch/dummy/powerswitch.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2021-06-24 -# @Filename: lvmnps/switch/dummy/powerswitch.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -import asyncio - -from sdsstools.logger import SDSSLogger - -from lvmnps.switch.powerswitchbase import PowerSwitchBase - - -__all__ = ["PowerSwitch"] - - -class PowerSwitch(PowerSwitchBase): - """Powerswitch class to manage the Digital Loggers Web power switch""" - - def __init__(self, name: str, config: dict, log: SDSSLogger): - super().__init__(name, config, log) - self.delay = self.config_get("delay", 0.0) - for o in self.outlets: - o.setState(0) - - async def start(self): - if not await self.isReachable(): - self.log.warning(f"{self.name} not reachable on start up") - await self.update(self.outlets) - - async def stop(self): - self.log.debug( - "For a moment, nothing happened. Then, after a second or so, " - "nothing continued to happen ..." - ) - - async def isReachable(self): - return True - - async def update(self, outlets): - pass - - async def switch(self, state, outlets): - for o in outlets: - self.log.debug(f"{self.name} set") - await asyncio.sleep(self.delay) - self.log.debug(f"{self.name} {outlets}") - o.setState(state) diff --git a/src/lvmnps/switch/factory.py b/src/lvmnps/switch/factory.py deleted file mode 100644 index 6cf0d1e..0000000 --- a/src/lvmnps/switch/factory.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2021-06-22 -# @Update: 2021-10-09 -# @Filename: lvmnps/switch/factory.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from typing import TYPE_CHECKING, Type - -from sdsstools.logger import SDSSLogger - -from lvmnps.exceptions import PowerException - -from .dli.powerswitch import DLIPowerSwitch -from .dummy.powerswitch import PowerSwitch as DummyPowerSwitch -from .iboot.powerswitch import PowerSwitch as IBootPowerSwitch -from .netio.powerswitch import PowerSwitch as NetIOPowerSwitch - - -if TYPE_CHECKING: - from lvmnps.switch.powerswitchbase import PowerSwitchBase - - -# from .iboot.powerswitch import PowerSwitch as IBootPowerSwitch - - -def powerSwitchFactory( - name: str, config: dict, log: SDSSLogger, simulate: bool = False -): - """Power switch factory method which helps the user to select the `.PowerSwitch` - class based on the configuration file that is selected. - - Parameters - ---------- - name - the name of the Dli Controller - config - The configuration dictionary from the configuration file .yml - log - The logger for logging - simulate - on True overwrite type from config with dummy - - """ - - def throwError(n, c): - """The method to throw the Exception.""" - raise PowerException(f"Power switch {n} with type {c['type']} not defined") - - factorymap: dict[str, Type[PowerSwitchBase]] = { - "dli": DLIPowerSwitch, - "netio": NetIOPowerSwitch, - "iboot": IBootPowerSwitch, - "dummy": DummyPowerSwitch, - } - - log.info(f"{simulate:-}") - return factorymap.get( - config["type"] if not simulate else "dummy", lambda n, c, _: throwError(n, c) - )(name, config, log) diff --git a/src/lvmnps/switch/iboot/iboot.py b/src/lvmnps/switch/iboot/iboot.py deleted file mode 100644 index f775804..0000000 --- a/src/lvmnps/switch/iboot/iboot.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2013, Luke Fitzgerald -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are those -of the authors and should not be interpreted as representing official policies, -either expressed or implied, of the FreeBSD Project.""" - -import asyncio -import logging -import socket -import struct - - -HELLO_STR = "hello-000" - -HEADER_STRUCT = struct.Struct(" List[OUTPUT]: - """Return list of all outputs in format of self.OUTPUT""" - - @abstractmethod - def _set_outputs(self, actions: Dict[int, ACTION]) -> None: - """Set multiple outputs.""" - - def get_outputs(self) -> List[OUTPUT]: - """Returns list of available sockets and their state""" - return self._get_outputs() - - def get_outputs_filtered(self, ids): - """ """ - outputs = self.get_outputs() - for i in ids: - try: - yield next(filter(lambda output: output.ID == i, outputs)) - except StopIteration: - raise UnknownOutputId("Invalid output ID") - - def get_output(self, id: int) -> OUTPUT: - """Get state of single socket by its id""" - outputs = self.get_outputs() - try: - return next(filter(lambda output: output.ID == id, outputs)) - except StopIteration: - raise UnknownOutputId("Invalid output ID") - - def set_outputs(self, actions: Dict[int, ACTION]) -> None: - """ - Set state of multiple outputs at once - >>> n.set_outputs({1: n.ACTION.ON, 2:n.ACTION.OFF}) - """ - # TODO verify if socket id's are in range - if self._write_access: - self._set_outputs(actions) - else: - raise AuthError("cannot write, without write access") - - def set_output(self, id: int, action: ACTION) -> None: - self.set_outputs({id: action}) - - def __repr__(self): - return f"" - - -class JsonDevice(Device): - def __init__( - self, url, auth_r=None, auth_rw=None, verify=None, skip_init=False, timeout=2 - ): - self._url = url - self._verify = verify - self.timeout = timeout - - # read-write can do read, so we don't need read-only permission - if auth_rw: - self._user = auth_rw[0] - self._pass = auth_rw[1] - self._write_access = True - elif auth_r: - self._user = auth_r[0] - self._pass = auth_r[1] - else: - raise AuthError("No auth provided.") - - if not skip_init: - self.init() - - def init(self): - # request information about the Device - r_json = self._get() - - self.NumOutputs = r_json["Agent"]["NumOutputs"] - self.DeviceName = r_json["Agent"]["DeviceName"] - self.SerialNumber = r_json["Agent"]["SerialNumber"] - - def get_info(self): - r_json = self._get() - r_json.pop("Outputs") - return r_json - - @staticmethod - def _parse_response(response: requests.Response) -> dict: - """ - Parse JSON response according to - https://www.netio-products.com/files/NETIO-M2M-API-Protocol-JSON.pdf - """ - - if response.status_code == 400: - raise CommunicationError("Control command syntax error") - - if response.status_code == 401: - raise AuthError("Invalid Username or Password") - - if response.status_code == 403: - raise AuthError("Insufficient permissions to write") - - if not response.ok: - raise CommunicationError("Communication with device failed") - - try: - rj = response.json() - except ValueError: - raise CommunicationError("Response does not contain valid json") - - return rj - - def _post(self, body: dict) -> dict: - try: - response = requests.post( - self._url, - data=json.dumps(body), - auth=requests.auth.HTTPBasicAuth(self._user, self._pass), - verify=self._verify, - timeout=self.timeout, - ) - except requests.exceptions.SSLError: - raise AuthError("Invalid certificate") - - return self._parse_response(response) - - def _get(self) -> dict: - try: - response = requests.get( - self._url, - auth=requests.auth.HTTPBasicAuth(self._user, self._pass), - verify=self._verify, - timeout=self.timeout, - ) - except requests.exceptions.SSLError: - raise AuthError("Invalid certificate") - - return self._parse_response(response) - - def _get_outputs(self) -> List[Device.OUTPUT]: - """ - Send empty GET request to the device. - Parse out the output states according to specification. - """ - - r_json = self._get() - - outputs = list() - - for output in r_json.get("Outputs"): - state = self.OUTPUT( - ID=output.get("ID"), - Name=output.get("Name"), - State=output.get("State"), - Action=self.ACTION(output.get("Action")), - Delay=output.get("Delay"), - Current=output.get("Current"), - PowerFactor=output.get("PowerFactor"), - Load=output.get("Load"), - Energy=output.get("Energy"), - ) - outputs.append(state) - return outputs - - def _set_outputs(self, actions: dict) -> dict: - outputs = [] - for id, action in actions.items(): - outputs.append({"ID": id, "Action": action}) - - body = {"Outputs": outputs} - - return self._post(body) - - # TODO verify response action diff --git a/src/lvmnps/switch/netio/Netio/__init__.py b/src/lvmnps/switch/netio/Netio/__init__.py deleted file mode 100644 index e89b5cb..0000000 --- a/src/lvmnps/switch/netio/Netio/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .Device import JsonDevice as Netio # Default device diff --git a/src/lvmnps/switch/netio/Netio/__main__.py b/src/lvmnps/switch/netio/Netio/__main__.py deleted file mode 100644 index 2f05ddc..0000000 --- a/src/lvmnps/switch/netio/Netio/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .cli import main - - -if __name__ == "__main__": - main() diff --git a/src/lvmnps/switch/netio/Netio/cli.py b/src/lvmnps/switch/netio/Netio/cli.py deleted file mode 100644 index 71c5734..0000000 --- a/src/lvmnps/switch/netio/Netio/cli.py +++ /dev/null @@ -1,397 +0,0 @@ -#!/usr/bin/env python3 -""" -Netio Command line interface -""" - -import argparse -import configparser -import os -import sys -import traceback -from urllib.parse import urlparse, urlunparse - -from typing import List - -import pkg_resources -import requests - -from . import Netio -from .exceptions import NetioException - - -def str2action(s: str) -> Netio.ACTION: - """Parse Device.ACTION, either by name or by integer representation""" - try: - return Netio.ACTION[s.upper()] - except KeyError or AttributeError: - try: - return Netio.ACTION(int(s)) - except ValueError: - raise argparse.ArgumentTypeError( - f"{s!r} is not a valid " f"{Netio.ACTION.__name__}" - ) - - -# all Device.ACTION choices, including INT -ACTION_CHOICES = [e.value for e in Netio.ACTION] + [e.name for e in Netio.ACTION] - -EPILOG = """ -Report bugs to: averner@netio.eu -project repository and documentation: https://github.com/netioproducts/PyNetio -released under MIT license by NETIO Products a.s. -""" - - -def get_arg(arg, config, name, env_name, section, default): - """ - argument is looked up in this order: - 1. argument itself - 2. specified section - 3. DEFAULT section - 4. param default - """ - if arg == default: - if env_name and env_name in os.environ: - return os.environ[env_name] - if section and config.has_option(section, name): - return config[section][name] - return config["DEFAULT"].get(name, default) - return arg - - -def get_ids(id_strs: List[str], num_outputs: int) -> List[int]: - """ - Generate a list of integer IDs from list of strings. - The list can either contain one string "ALL" or an individual IDs - - >> get_ids(["ALL"], 4) - [1, 2, 3, 4] - """ - all_ids = range(1, num_outputs + 1) - all_outputs_mode = False - individual_outputs_mode = False - result = [] - - for id_str in id_strs: - if id_str.lower() == "all": - if individual_outputs_mode: - raise NetioException("Expecting either individual outputs IDs or 'ALL'") - all_outputs_mode = True - result = list(all_ids) - elif id_str.isdecimal() and int(id_str) in all_ids: - if all_outputs_mode: - raise NetioException("Expecting either individual outputs IDs or 'ALL'") - individual_outputs_mode = True - result.append(int(id_str)) - else: - raise NetioException( - f"Invalid output ID '{id_str}', " - f"valid range is {1}-{num_outputs + 1} or 'ALL'" - ) - - return result - - -def get_output_actions(ids_and_actions, num_outputs): - """ - Parse out pairs of ID + ACTION from iterable `ids_and_actions` - parse 'all' keyword if present. - input can't have combination of all and individual IDs - - return dictionary containing ID: ACTION pairs - """ - max_id = num_outputs + 1 - all_ids = range(1, max_id) - all_outputs_mode = False - individual_outputs_mode = False - result = {} - - if len(ids_and_actions) % 2 != 0: - raise NetioException( - "Expecting ID ACTION pairs but got an " - f"odd number of arguments ({len(ids_and_actions)})" - ) - pairs = zip(ids_and_actions[::2], ids_and_actions[1::2]) - - for pair in list(pairs): - id_str, action_str = pair - - if id_str.lower() == "all": - if individual_outputs_mode: - raise NetioException("Expecting either individual outputs IDs or 'ALL'") - all_outputs_mode = True - action = str2action(action_str) - result = dict(zip(all_ids, [action] * num_outputs)) - elif id_str.isdecimal() and int(id_str) in all_ids: - if all_outputs_mode: - raise NetioException("Expecting either individual outputs IDs or 'ALL'") - individual_outputs_mode = True - id = int(id_str) - if id in result: - raise NetioException( - "Multiple actions given for id " - "'{}' but expecting only one".format(id) - ) - action = str2action(action_str) - result[id] = action - else: - raise NetioException( - f"Invalid output ID '{id_str}', valid range is {1}-{max_id} or 'ALL'" - ) - - return result - - -def load_config(args): - """Load configuration file and other other configs""" - - config = configparser.ConfigParser( - {"user": "", "password": "", "no_cert_warning": ""} - ) - if not args.conf: - args.conf = os.environ.get("NETIO_CONFIG") - - if args.conf: - try: - with open(args.conf) as fp: - config.read_file(fp, args.conf) - except (TypeError, OSError, FileNotFoundError, configparser.Error) as e: - raise NetioException(f"Failed reading config ({e.__class__.__name__})") - - # resolve the device alias - if config.has_option(args.device, "url"): - args.device = config[args.device]["url"] - - u = urlparse(args.device) - - args.cert = get_arg(args.cert, config, "cert", None, u.netloc, True) - args.user = get_arg(args.user, config, "user", "NETIO_USER", u.netloc, None) - args.password = get_arg( - args.password, config, "password", "NETIO_PASSWORD", u.netloc, None - ) - args.no_cert_warning = get_arg( - args.no_cert_warning, config, "no_cert_warning", None, u.netloc, False - ) - - # resolve the path of cert relative to configuration path - basedir = ( - os.path.dirname(args.conf) if args.conf else os.path.dirname(os.path.curdir) - ) - args.cert = ( - args.cert if isinstance(args.cert, bool) else os.path.join(basedir, args.cert) - ) - - return args - - -def parse_args(): - parser = argparse.ArgumentParser(epilog=EPILOG) # prog='netio') - - parser.add_argument( - "device", metavar="DEVICE", action="store", help="Netio device URL" - ) - - parser.add_argument( - "-u", - "--user", - action="store", - dest="user", - metavar="U", - help="M2M API username", - ) - parser.add_argument( - "-p", - "--password", - action="store", - dest="password", - metavar="P", - help="M2M API password", - ) - - parser.add_argument( - "-C", - "--cert", - action="store_false", - dest="cert", - default=True, - help="HTTPS Certificate", - ) - parser.add_argument( - "-c", - "--config", - action="store", - dest="conf", - metavar="CFG", - help="Configuration file", - ) - parser.add_argument( - "-v", "--verbose", action="count", default=0, help="increase verbosity" - ) - parser.add_argument( - "--no-cert-warning", - action="store_true", - help="Disable warnings about certificate's subjectAltName versus commonName", - ) - - try: - version = pkg_resources.require("Netio")[0].version - except pkg_resources.DistributionNotFound: - version = "Unknown" - - parser.add_argument( - "--version", action="version", version=f"%(prog)s (version {version})" - ) - - command_parser = parser.add_subparsers(metavar="COMMAND", help="device command") - command_parser.required = True - - # GET command subparser - get_parser = command_parser.add_parser( - "get", help="GET output state", aliases=["GET", "G", "g"] - ) - get_parser.add_argument( - "id", - metavar="ID", - nargs="*", - default=["ALL"], - help="Output ID. All if not specified", - ) - get_parser.set_defaults(func=command_get) - get_parser.add_argument( - "-d", "--delimiter", action="store", dest="delim", default="\t", help="" - ) - get_parser.add_argument( - "--no-header", action="store_true", help="don't print column description" - ) - get_parser.add_argument( - "--action-int", action="store_true", help="print action as integer" - ) - - # SET command subparser - set_parser = command_parser.add_parser( - "set", help="SET output state", aliases=["SET", "S", "s"] - ) - set_parser.set_defaults(func=command_set) - # We are using a forged meta variable to get ID and action pairs into the - # help. The actual result is still a list of individual parameters and - # parsing the pairs is done later by get_output_actions. - set_parser.add_argument( - "id_and_action", - metavar="ID ACTION", - nargs="+", - help="output ID and action pairs" - " (valid actions: " - f"{[a.name for a in Netio.ACTION]})", - ) - - # INFO command subparser - info_parser = command_parser.add_parser( - "info", help="show device info", aliases=["INFO", "I", "i"] - ) - info_parser.set_defaults(func=command_info) - - return parser.parse_args() - - -def print_traceback(args, file=sys.stderr): - """ - Print traceback if requested by argument '--verbose'. A traceback is also - considered as requested if arguments have not been parsed yet (args are - None). - """ - if not args or (hasattr(args, "verbose") and args.verbose): - traceback.print_exc(file=file) - - -def main(): - """Main entry point of the app""" - args = None - - try: - args = parse_args() - args = load_config(args) - - if args.no_cert_warning: - requests.packages.urllib3.disable_warnings() - - u = urlparse(args.device) - u = u._replace(path="/netio.json") if not u.path else u # no path specified - - # try to run the specified command, on fail print nice fail message - device = Netio( - urlunparse(u), - auth_rw=(args.user, args.password), - verify=args.cert, - skip_init=True, - ) - args.func(device, args) - except NetioException as e: - print(e.args[0], file=sys.stderr) - print_traceback(args) - sys.exit(1) - except Exception as e: - print("Internal error: ", e, file=sys.stderr) - print_traceback(args) - sys.exit(1) - - -def command_set(device: Netio, args: argparse.Namespace) -> None: - """Set the output specified in args.id to args.action""" - - device.init() - - actions = get_output_actions(args.id_and_action, device.NumOutputs) - device.set_outputs(actions) - - -def command_get(device: Netio, args: argparse.Namespace) -> None: - """Print the state of the output and exit""" - - # init because we need to know NumOutputs so we can generate id list for "ALL" - # This initialization could be skipped, but that would require different handling - # for 'all' parameter - device.init() - - ids = get_ids(args.id, device.NumOutputs) # returns single or range - outputs = list(device.get_outputs_filtered(ids)) - - if not args.no_header: - print( - "id", - "State", - "Action", - "Delay", - "Current", - "PFactor", - "Load", - "Energy", - "Name", - sep=args.delim, - ) - for o in outputs: - action = o.Action.name if not args.action_int else o.Action.value - print( - o.ID, - o.State, - action, - o.Delay, - o.Current, - o.PowerFactor, - o.Load, - o.Energy, - o.Name, - sep=args.delim, - ) - - -def command_info(device: Netio, args: argparse.Namespace) -> None: - """Print out all data from device info""" - for key, data in device.get_info().items(): - print(key) - for subkey, value in data.items(): - pad = 18 - len(subkey) - print(" ", subkey, " " * pad, value) - - -if __name__ == "__main__": - main() diff --git a/src/lvmnps/switch/netio/Netio/exceptions.py b/src/lvmnps/switch/netio/Netio/exceptions.py deleted file mode 100644 index b1e4919..0000000 --- a/src/lvmnps/switch/netio/Netio/exceptions.py +++ /dev/null @@ -1,14 +0,0 @@ -class NetioException(Exception): - """Base exception for Device""" - - -class CommunicationError(NetioException): - """Communication with Device failed""" - - -class AuthError(NetioException): - """Authentication missing, or invalid""" - - -class UnknownOutputId(NetioException): - """Unknown output ID""" diff --git a/src/lvmnps/switch/netio/__init__.py b/src/lvmnps/switch/netio/__init__.py deleted file mode 100644 index 483a3a7..0000000 --- a/src/lvmnps/switch/netio/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2022-07-11 -# @Filename: lvmnps/switch/netio/__init__.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) diff --git a/src/lvmnps/switch/netio/powerswitch.py b/src/lvmnps/switch/netio/powerswitch.py deleted file mode 100644 index 0091799..0000000 --- a/src/lvmnps/switch/netio/powerswitch.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2022-07-11 -# @Filename: lvmnps/switch/netio/powerswitch.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -import asyncio - -from sdsstools.logger import SDSSLogger - -from lvmnps.switch.netio.Netio import Netio -from lvmnps.switch.powerswitchbase import PowerSwitchBase - - -__all__ = ["PowerSwitch"] - - -class PowerSwitch(PowerSwitchBase): - """Powerswitch class to manage the Netio Web power switch""" - - def __init__(self, name: str, config: dict, log: SDSSLogger): - super().__init__(name, config, log) - - for o in self.outlets: - o.setState(0) - - hostname = self.config_get("hostname") - username = self.config_get("username", "netio") - password = self.config_get("password", "netio") - - self.con_url = f"http://{hostname}/netio.json" - self.con_args = {"auth_rw": (username, password), "verify": True} - self.portsnum = int(self.config_get("ports.num", "4")) - - self.netio = Netio(self.con_url, **self.con_args, skip_init=True) - - async def start(self): - await self.update(self.outlets) - - async def stop(self): - self.log.debug( - "For a moment, nothing happened. Then, after a second or so, " - "nothing continued to happen ..." - ) - - async def isReachable(self): - loop = asyncio.get_event_loop() - try: - await loop.run_in_executor(None, self.netio.init) - return True - except Exception as ex: - self.log.error(f"{self.name}: {ex}") - return False - - async def update(self, outlets): - loop = asyncio.get_event_loop() - try: - relays = await loop.run_in_executor(None, self.netio.get_outputs) - for o in outlets: - o.setState( - relays[o.portnum - 1].State if o.portnum <= len(relays) else -1 - ) - - except Exception as ex: - self.log.error(f"{self.name}: {ex}") - for o in outlets: - o.setState(-1) - - async def switch(self, state, outlets): - loop = asyncio.get_event_loop() - try: - await loop.run_in_executor( - None, - self.netio.set_outputs, - { - o.portnum: Netio.ACTION.ON if state else Netio.ACTION.OFF - for o in outlets - }, - ) - self.log.debug(f"{self.name} {outlets}") - for o in outlets: - o.setState(state) - - except Exception as ex: - self.log.error(f"{self.name}: {ex}") - for o in outlets: - o.setState(-1) diff --git a/src/lvmnps/switch/outlet.py b/src/lvmnps/switch/outlet.py deleted file mode 100644 index 8d9a25e..0000000 --- a/src/lvmnps/switch/outlet.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2021-06-22 -# @Filename: lvmnps/switch/outlet.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from typing import TYPE_CHECKING - - -if TYPE_CHECKING: - from .powerswitchbase import PowerSwitchBase - - -class Outlet(object): - """Outlet class to manage the power switch. - - Parameters - ---------- - switch - The parent `.PowerSwitchBase` instance to which this outlet is associated with. - name - The name of the outlet. - portnum - The number of the port. - description - The description about the outlet. - state - The state of the outlet (on: 1, off: 0). - - """ - - def __init__( - self, - switch: PowerSwitchBase, - name: str, - portnum: int, - description: str | None = None, - state: int = 0, - ): - self.switch = switch - self.name = name if name else f"{self.switch.name}.port{portnum}" - self.portnum = portnum - - default_description = f"{self.switch.name} Port {portnum}" - self.description = description if description else default_description - - self.inuse = bool(name) or bool(description) - self.state = state - - def __str__(self): - return f"#{self.portnum}:{self.name}={self.state}" - - def __repr__(self): - return self.__str__() - - @staticmethod - def parse(value): - """Parse the input data for ON/OFF.""" - - if isinstance(value, str): - value = value.lower() - - if value in ["off", "0", 0, False]: - return 0 - if value in ["on", "1", 1, True]: - return 1 - - return -1 - - def setState(self, value): - """Class method: Set the state of the outlet inside the class.""" - self.state = Outlet.parse(value) - - def isOn(self): - """Return the state of the outlet.""" - return self.state == 1 - - def isOff(self): - """Return the state of the outlet.""" - return self.state == 0 - - def isValid(self): - """Return the validity of the outlet.""" - return self.state == -1 - - def toDict(self): - """Return the dictionary describing the status of the outlet.""" - return { - "state": self.state, - "descr": self.description, - "switch": self.switch.name, - "port": self.portnum, - } diff --git a/src/lvmnps/switch/powerswitchbase.py b/src/lvmnps/switch/powerswitchbase.py deleted file mode 100644 index 5c561ee..0000000 --- a/src/lvmnps/switch/powerswitchbase.py +++ /dev/null @@ -1,269 +0,0 @@ -# -*- coding: utf-8 -*- -# -# @Author: Florian Briegel (briegel@mpia.de) -# @Date: 2021-06-24 -# @Filename: lvmnps/switch/powerswitchbase.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from abc import abstractmethod - -from sdsstools.logger import SDSSLogger - -from lvmnps.switch.outlet import Outlet - - -__all__ = ["PowerSwitchBase"] - - -class PowerSwitchBase(object): - """PowerSwitchBase class for multiple power switches from different manufacturers. - - The Powerswitch classes will inherit from the `.PowerSwitchBase` class. - - Parameters - ---------- - name - A name identifying the power switch. - config - The configuration defined on the .yaml file under ``/etc/lvmnps.yml``. - log - The logger for logging. - - """ - - def __init__(self, name: str, config: dict, log: SDSSLogger | None = None): - self.name = name - self.log = log or SDSSLogger(f"powerswitchbase.{name}") - self.config = config - - numports = self.config_get("ports.number_of_ports", 8) - if numports is None: - raise ValueError(f"{name}: unknown number of ports.") - self.numports: int = numports - - self.outlets = [ - Outlet( - self, - self.config_get(f"ports.{portnum}.name"), - portnum, - self.config_get(f"ports.{portnum}.desc"), - -1, - ) - for portnum in range(1, self.numports + 1) - ] - - self.onlyusedones = self.config_get("ouo", True) - self.log.debug(f"Only used ones: {self.onlyusedones}") - - def config_get(self, key, default=None): - """Read the configuration and extract the data as a structure that we want. - - Notice: DOESN'T work for keys with dots !!! - - Parameters - ---------- - key - The tree structure as a string to extract the data. - For example, if the configuration structure is :: - - ports: - 1: - desc: "Hg-Ar spectral callibration lamp" - - You can input the key as - ``ports.1.desc`` to take the information "Hg-Ar spectral callibration lamp". - - """ - - def g(config, key, d=None): - """Internal function for parsing the key from the configuration. - - Parameters - ---------- - config - config from the class member, which is saved from the class instance - key - The tree structure as a string to extract the data. - For example, if the configuration structure is :: - - ports: - num:1 - 1: - desc: "Hg-Ar spectral callibration lamp" - - You can input the key as "ports.1.desc" to take the information - "Hg-Ar spectral callibration lamp" - - """ - - k = key.split(".", maxsplit=1) - c = config.get( - k[0] if not k[0].isnumeric() else int(k[0]) - ) # keys can be numeric - - return ( - d - if c is None - else c - if len(k) < 2 - else g(c, k[1], d) - if isinstance(c, dict) - else d - ) - - return g(self.config, key, default) - - def findOutletByName(self, name: str): - """Find the outlet by the name, comparing with the name from the Outlet object. - - Parameters - ---------- - name - The string to compare with the name in Outlet instance. - """ - for o in self.outlets: - if o.name.lower() == name.lower(): - return o - - def collectOutletsByNameAndPort( - self, - name: str | None = None, - portnum: int | None = None, - ): - """Collects the outlet by the name and ports, - comparing with the name and ports from the Outlet object. - - Parameters - ---------- - name - The string to compare with the name in Outlet instance. - portnum - The integer for indicating each Outlet instances. If zero or `None`, - identifies the outlet only by name. - - Returns - ------- - outlets - A list of `.Outlet` that match the name and port number. If ``name=None``, - the outlet matching the port number is returned. If both ``name`` and - ``portnum`` are `None`, a list with all the outlets connected to this - switch is returned. - - """ - - if not name or name == self.name: - if portnum: - if portnum > self.numports: - return [] - return [self.outlets[portnum - 1]] - else: - outlets = [] - - for o in self.outlets: - if o.inuse or not self.onlyusedones: - outlets.append(o) - - return outlets - else: - o = self.findOutletByName(name) - if o: - return [o] - - return [] - - async def setState( - self, - state: bool | int, - name: str | None = None, - portnum: int | None = None, - ): - """Set the state of the Outlet instance to On/Off. (On = 1, Off = 0). - - Note that dependending on the values passed to ``name`` and ``portnum``, - multiple outlets may be commanded. - - Parameters - ---------- - state - The boolean value (True, False) to set the state inside the Outlet object. - name - The string to compare with the name in Outlet instance. - portnum - The integer for indicating each Outlet instances. - - """ - - state_int = Outlet.parse(state) - if state_int == -1: - raise ValueError(f"{self.name}: cannot parse state {state!r}.") - - return await self.switch( - state_int, - self.collectOutletsByNameAndPort(name, portnum), - ) - - async def statusAsDict(self, name: str | None = None, portnum: int | None = None): - """Get the status of the `.Outlets` as a dictionary. - - Parameters - ---------- - name - The string to compare with the name in Outlet instance. - ``name`` can be a switch or an outlet name. - portnum - The integer for indicating each Outlet instances. - - """ - - outlets = self.collectOutletsByNameAndPort(name, portnum) - - await self.update(outlets) - - status = {} - for o in outlets: - status[f"{o.name}"] = o.toDict() - - return status - - @abstractmethod - async def start(self): - """Starts the switch instance, potentially connecting to the device server.""" - pass - - @abstractmethod - async def stop(self): - """Stops the connection to the switch server.""" - pass - - @abstractmethod - async def isReachable(self): - """Verify we can reach the switch. Returns `True` if ok.""" - pass - - @abstractmethod - async def update(self, outlets: list[Outlet] | None): - """Retrieves the status of a list of outlets and updates the internal mapping. - - Parameters - ---------- - outlets - A list of `.Outlets` to update. If `None`, all outlets are updated. - - """ - pass - - @abstractmethod - async def switch(self, state: int, outlets: list[Outlet]): - """Changes the state of an outlet. - - Parameters - ---------- - state - The final state for the outlets. 0: off, 1: on. - outlets - A list of `.Outlets` which status will be updated. - - """ - pass diff --git a/tests/config.yaml b/tests/config.yaml new file mode 100644 index 0000000..aaf8c39 --- /dev/null +++ b/tests/config.yaml @@ -0,0 +1,5 @@ +--- +actor: + name: lvmnps.test + host: localhost + port: 5672 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index c8d6ec9..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import annotations - -import os - -import pytest - -import clu.testing -from sdsstools import read_yaml_file -from sdsstools.logger import get_logger - -from lvmnps.actor.actor import NPSActor -from lvmnps.switch.factory import powerSwitchFactory - - -@pytest.fixture() -def test_config(): - yield read_yaml_file(os.path.join(os.path.dirname(__file__), "test_switch.yml")) - - -@pytest.fixture -def switches(test_config): - assert "switches" in test_config - - switches = [] - for name, conf in test_config["switches"].items(): - try: - switches.append(powerSwitchFactory(name, conf, get_logger("test"))) - except Exception as ex: - print(f"Error in power switch factory {type(ex)}: {ex}") - - return switches - - -@pytest.fixture() -async def actor(switches, test_config: dict): - _actor = NPSActor.from_config(test_config) - _actor = await clu.testing.setup_test_actor(_actor) # type: ignore - - _actor.parser_args = [{switch.name: switch for switch in switches}] - await _actor.start() - - yield _actor - - _actor.mock_replies.clear() - await _actor.stop() diff --git a/tests/test_actor.py b/tests/test_actor.py deleted file mode 100644 index 8503b5f..0000000 --- a/tests/test_actor.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import annotations - -import pytest - -from lvmnps.actor.actor import AMQPActor, NPSActor - - -async def test_actor(actor: NPSActor): - assert actor - - -async def test_ping(actor: NPSActor): - command = await actor.invoke_mock_command("ping") - await command - - assert command.status.did_succeed - assert len(command.replies) == 2 - assert command.replies[1].message["text"] == "Pong." - - -async def test_actor_no_config(): - with pytest.raises(RuntimeError): - NPSActor.from_config(None) - - -async def test_actor_start(switches, test_config: dict, mocker): - actor = NPSActor.from_config(test_config) - mocker.patch.object(AMQPActor, "start") - - actor.parser_args = [{switch.name: switch for switch in switches}] - - for switch in switches: - mocker.patch.object(switch, "start") - - await actor.start() - - assert len(actor.parser_args[0].keys()) == len(switches) - - await actor.stop() - - -async def test_actor_start_one_fails(switches, test_config: dict, mocker): - actor = NPSActor.from_config(test_config) - mocker.patch.object(AMQPActor, "start") - - actor.parser_args = [{switch.name: switch for switch in switches}] - - for ii, switch in enumerate(switches): - mocker.patch.object( - switch, - "start", - side_effect=None if ii != 1 else RuntimeError, - ) - - await actor.start() - - assert len(actor.parser_args[0].keys()) == len(switches) - 1 - - await actor.stop() diff --git a/tests/test_async.py b/tests/test_async.py deleted file mode 100644 index ca2d4e2..0000000 --- a/tests/test_async.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import annotations - -import asyncio - -from typing import Any - -from lvmnps.actor.actor import NPSActor - - -async def test_async_onoff(switches, actor: NPSActor): - # status check of nps_dummy_1 port1 - assert actor - - assert switches[3].name == "slow" - assert switches[3].outlets[0].name == "slow" - assert switches[3].outlets[0].state == 0 - - assert switches[4].name == "fast" - assert switches[4].outlets[0].name == "fast" - assert switches[4].outlets[0].state == 0 - - task = [] - task.append(actor.invoke_mock_command("on slow 1")) - task.append(actor.invoke_mock_command("on fast 1")) - - await asyncio.gather(*task) - - status_task = [] - status_task.append(actor.invoke_mock_command("status slow 1")) - status_task.append(actor.invoke_mock_command("status fast 1")) - - status_before: Any = await asyncio.gather(*status_task) - assert status_before[0].replies[-1].message["status"]["slow"]["slow"]["state"] == 0 - assert status_before[1].replies[-1].message["status"]["fast"]["fast"]["state"] == 1 - - await asyncio.sleep(2) - - status_task = [] - status_task.append(actor.invoke_mock_command("status slow 1")) - status_task.append(actor.invoke_mock_command("status fast 1")) - - status_after: Any = await asyncio.gather(*status_task) - assert status_after[0].replies[-1].message["status"]["slow"]["slow"]["state"] == 1 - assert status_after[1].replies[-1].message["status"]["fast"]["fast"]["state"] == 1 diff --git a/tests/test_dli.py b/tests/test_dli.py deleted file mode 100644 index 5302ab0..0000000 --- a/tests/test_dli.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import annotations - -import os - -import pytest - -from sdsstools import read_yaml_file - -from lvmnps.switch.dli.powerswitch import DLIPowerSwitch - - -@pytest.fixture -async def dli_switches(mocker): - root = os.path.dirname(__file__) - config = read_yaml_file(os.path.join(root, "test_dli_switch.yml")) - - switches = [] - for switch_name in config["switches"]: - switch = DLIPowerSwitch(switch_name, config["switches"][switch_name]) - - # Fake the client reply to a get(/relay/outlets) - get_mock = mocker.MagicMock(status_code=200) - - # Build a fake reply with two outlets defined and six empty ones - get_mock.json.return_value = [ - {"state": False, "name": "Outlet 1"}, - {"state": True, "name": "Outlet 2"}, - ] - get_mock.json.return_value += 6 * [{"state": False, "name": ""}] - - # Patch the client to use the mocked get method. - mocker.patch.object( - switch.dli.client, - "get", - return_value=get_mock, - ) - - # Also mock PUT - mocker.patch.object( - switch.dli.client, - "put", - return_value=mocker.MagicMock(status_code=204), - ) - - await switch.start() - - switches.append(switch) - - yield switches - - for switch in switches: - await switch.stop() - - -async def test_dli_power_switch(dli_switches: list[DLIPowerSwitch]): - switch = dli_switches[0] - - assert switch.name == "DLI-01" - assert len(switch.outlets) == 8 - assert switch.outlets[1].inuse is False - - -async def test_dli_power_switch_handle_undefined(dli_switches: list[DLIPowerSwitch]): - switch = dli_switches[0] - switch.onlyusedones = False - - await switch.start() - - assert switch.name == "DLI-01" - assert len(switch.outlets) == 8 - - assert switch.outlets[1].name == "Outlet 2" - assert switch.outlets[1].inuse is True - assert (await switch.isReachable()) is True - - -async def test_dli_on(dli_switches: list[DLIPowerSwitch]): - switch = dli_switches[0] - - assert switch.outlets[0].state == 0 - - await switch.switch(True, [switch.outlets[0]]) - - -async def test_dli_off(dli_switches: list[DLIPowerSwitch]): - switch = dli_switches[0] - - await switch.switch(False, [switch.outlets[0]]) - - -async def test_dli_verify_fails(dli_switches: list[DLIPowerSwitch], mocker): - switch = dli_switches[0] - mocker.patch.object(switch.dli, "verify", side_effect=ValueError) - - with pytest.raises(RuntimeError): - await switch.start() - - -def test_dli_missing_credentials(): - with pytest.raises(ValueError): - DLIPowerSwitch("test", {}) - - -async def test_dli_switch_fails(dli_switches: list[DLIPowerSwitch], mocker): - switch = dli_switches[0] - mocker.patch.object(switch.dli, "on", side_effect=ValueError) - - with pytest.raises(RuntimeError): - await switch.switch(True, [switch.outlets[0]]) - - -async def test_dli_update_fails(dli_switches: list[DLIPowerSwitch], mocker): - switch = dli_switches[0] - mocker.patch.object(switch.dli, "status", side_effect=ValueError) - - with pytest.raises(RuntimeError): - await switch.update([switch.outlets[0]]) - - assert switch.outlets[0].state == -1 - - -async def test_dli_update_unreachable(dli_switches: list[DLIPowerSwitch], mocker): - switch = dli_switches[0] - switch.reachable = False - - await switch.update([switch.outlets[0]]) - - assert switch.outlets[0].state == -1 diff --git a/tests/test_dli_switch.yml b/tests/test_dli_switch.yml deleted file mode 100644 index 2bc1606..0000000 --- a/tests/test_dli_switch.yml +++ /dev/null @@ -1,15 +0,0 @@ -# A dictionary of controller name to NPS controller connection parameters. -switches: - DLI-01: - type: dli - name: DLI-01 - hostname: 10.7.45.22 - user: 'admin' - password: 'rLXR3KxUqiCPGvA' - onoff_timeout: 3 - ouo: True - ports: - number_of_ports: 8 - 1: - name: 'Outlet 1' - desc: '' diff --git a/tests/test_onoff.py b/tests/test_onoff.py deleted file mode 100644 index d8e8235..0000000 --- a/tests/test_onoff.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import annotations - -import asyncio - -from lvmnps.actor.actor import NPSActor - - -async def test_onoff(switches, actor: NPSActor): - assert switches[0].name == "nps_dummy_1" - assert switches[0].outlets[0].name == "port1" - assert switches[0].outlets[0].state == 0 - - # switch on nps_dummy_1 port1 - command = await actor.invoke_mock_command("on nps_dummy_1 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 4 - assert command.replies[-2].message["status"]["nps_dummy_1"]["port1"]["state"] == 1 - - assert switches[0].outlets[0].state == 1 - - # switch off nps_dummy_1 port1 - command = await actor.invoke_mock_command("off nps_dummy_1 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 4 - assert command.replies[-2].message["status"]["nps_dummy_1"]["port1"]["state"] == 0 - - assert switches[0].outlets[0].state == 0 - - # switch skye.nps port 1 - assert switches[1].name == "skye.nps" - assert switches[1].outlets[0].name == "skye.pwi" - switches[1].outlets[0].state = 0 - assert switches[1].outlets[0].state == 0 - - # switch on skye.nps port1 - command = await actor.invoke_mock_command("on skye.nps 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 4 - assert command.replies[-2].message["status"]["skye.nps"]["skye.pwi"]["state"] == 1 - assert switches[1].outlets[0].state == 1 - - # switch off skye.nps port1 - command = await actor.invoke_mock_command("off skye.nps 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 4 - assert command.replies[-2].message["status"]["skye.nps"]["skye.pwi"]["state"] == 0 - assert switches[1].outlets[0].state == 0 - - -async def test_status_already_on(switches, actor: NPSActor): - assert actor - - assert switches[0].name == "nps_dummy_1" - assert switches[0].outlets[0].name == "port1" - switches[0].outlets[0].state = 0 - assert switches[0].outlets[0].state == 0 - - # switch on nps_dummy_1 port1 - command = await actor.invoke_mock_command("on nps_dummy_1 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 4 - assert command.replies[-2].message["status"]["nps_dummy_1"]["port1"]["state"] == 1 - - # switch on nps_dummy_1 port1 - command = await actor.invoke_mock_command("on nps_dummy_1 1") - await command - assert command.status.did_succeed - - -async def test_status_already_off(switches, actor: NPSActor): - assert actor - assert switches[0].name == "nps_dummy_1" - assert switches[0].outlets[0].name == "port1" - switches[0].outlets[0].state = 0 - assert switches[0].outlets[0].state == 0 - - # switch on nps_dummy_1 port1 - command = await actor.invoke_mock_command("on nps_dummy_1 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 4 - assert command.replies[-2].message["status"]["nps_dummy_1"]["port1"]["state"] == 1 - - # switch off nps_dummy_1 port1 - command = await actor.invoke_mock_command("off nps_dummy_1 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 4 - assert command.replies[-2].message["status"]["nps_dummy_1"]["port1"]["state"] == 0 - - # switch off nps_dummy_1 port1 - command = await actor.invoke_mock_command("off nps_dummy_1 1") - await command - assert command.status.did_succeed - - -async def test_on_succeed(switches, actor: NPSActor): - assert actor - assert switches[0].name == "nps_dummy_1" - assert switches[0].outlets[0].name == "port1" - switches[0].outlets[0].state = -1 - assert switches[0].outlets[0].state == -1 - - # switch on nps_dummy_1 port1 - command = await actor.invoke_mock_command("on nps_dummy_1 1") - await command - assert command.status.did_succeed - - -async def test_off_succeed(switches, actor: NPSActor): - assert actor - assert switches[0].name == "nps_dummy_1" - assert switches[0].outlets[0].name == "port1" - switches[0].outlets[0].state = -1 - assert switches[0].outlets[0].state == -1 - - # switch on nps_dummy_1 port1 - command = await actor.invoke_mock_command("off nps_dummy_1 1") - await command - assert command.status.did_succeed - - -async def test_status_off_after(switches, actor: NPSActor): - assert actor - assert switches[0].name == "nps_dummy_1" - assert switches[0].outlets[0].name == "port1" - switches[0].outlets[0].state = 0 - assert switches[0].outlets[0].state == 0 - - # switch on nps_dummy_1 port1 - status = [] - - status.append( - asyncio.create_task(actor.invoke_mock_command("on --off-after 3 nps_dummy_1 1")) - ) - status.append(asyncio.create_task(say_after(0.2, actor))) - - status_result = list(await asyncio.gather(*status)) - - status = status_result[1].replies[-1].message["status"] - assert status["nps_dummy_1"]["port1"]["state"] == 1 # noqa: W503 - - await asyncio.sleep(2) - - command = await actor.invoke_mock_command("status nps_dummy_1 1") - await command - assert command.status.did_succeed - assert command.replies[-1].message["status"]["nps_dummy_1"]["port1"]["state"] == 0 - assert switches[0].outlets[0].state == 0 - - -async def say_after(delay, actor_mock): - await asyncio.sleep(delay) - command = await actor_mock.invoke_mock_command("status nps_dummy_1 1") - await command - assert command.status.did_succeed - return command diff --git a/tests/test_outlets.py b/tests/test_outlets.py deleted file mode 100644 index 6c9222d..0000000 --- a/tests/test_outlets.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# @Author: José Sánchez-Gallego (gallegoj@uw.edu) -# @Date: 2022-05-22 -# @Filename: test_outlets.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from typing import TYPE_CHECKING - - -if TYPE_CHECKING: - from lvmnps.actor import NPSActor - - -async def test_command_outlets(actor: NPSActor): - cmd = await (await actor.invoke_mock_command("outlets")) - assert cmd.status.did_succeed - - assert len(cmd.replies) == 2 - assert cmd.replies.get("outlets") == [ - "port1", - "skye.what.ever", - "skyw.what.ever", - "skye.pwi", - "skyw.pwi", - "slow", - "fast", - ] - - -async def test_command_outlets_switchname(actor: NPSActor): - cmd = await (await actor.invoke_mock_command("outlets nps_dummy_1")) - assert cmd.status.did_succeed - - assert len(cmd.replies) == 2 - assert cmd.replies.get("outlets") == ["port1", "skye.what.ever", "skyw.what.ever"] - - -async def test_command_outlets_switchname_unknown(actor: NPSActor): - cmd = await (await actor.invoke_mock_command("outlets blah")) - assert cmd.status.did_fail diff --git a/tests/test_status.py b/tests/test_status.py deleted file mode 100644 index 9fae272..0000000 --- a/tests/test_status.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import annotations - -import pytest - -from lvmnps.actor.actor import NPSActor - - -async def test_status(switches, actor: NPSActor): - # status check of nps_dummy_1 port1 - assert actor - command = await actor.invoke_mock_command("status nps_dummy_1 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 2 - assert command.replies[-1].message["status"]["nps_dummy_1"]["port1"]["state"] == 0 - - assert switches[0].name == "nps_dummy_1" - assert switches[0].outlets[0].name == "port1" - assert switches[0].outlets[0].state == 0 - - # status check of nps_dummy_1 port1 - assert actor - command = await actor.invoke_mock_command("status nps_dummy_1 1") - await command - assert command.status.did_succeed - assert len(command.replies) == 2 - assert command.replies[-1].message["status"]["nps_dummy_1"]["port1"]["state"] == 0 - - # switch status - command = await actor.invoke_mock_command("status nps_dummy_1") - await command - assert command.status.did_succeed - assert len(command.replies) == 2 - - status = command.replies[-1].message["status"] - assert status["nps_dummy_1"]["port1"]["state"] == 0 - assert status["nps_dummy_1"]["skye.what.ever"]["state"] == 0 - assert status["nps_dummy_1"]["skyw.what.ever"]["state"] == 0 - - # status of all available switches - command = await actor.invoke_mock_command("status") - await command - assert command.status.did_succeed - status = command.replies[-1].message["status"] - - assert status["nps_dummy_1"]["port1"]["state"] == 0 - assert status["nps_dummy_1"]["skye.what.ever"]["state"] == 0 - assert status["nps_dummy_1"]["skyw.what.ever"]["state"] == 0 - - -async def test_status_bad_switchname(actor: NPSActor): - command = await actor.invoke_mock_command("status BLAH") - await command - - assert command.status.did_fail - assert command.replies.get("error") == "Unknown switch BLAH." - - -@pytest.mark.parametrize("switchname", ["", "nps_dummy_1"]) -async def test_status_not_reachable_error(actor: NPSActor, switchname, mocker): - for switch in actor.parser_args[0].values(): - mocker.patch.object(switch, "isReachable", return_value=False) - - command = await actor.invoke_mock_command(f"status {switchname}") - await command - - assert command.status.did_fail - assert command.replies.get("error") == "Unable to find matching outlets." diff --git a/tests/test_switch.yml b/tests/test_switch.yml deleted file mode 100644 index a1f9cbc..0000000 --- a/tests/test_switch.yml +++ /dev/null @@ -1,51 +0,0 @@ -# A dictionary of controller name to NPS controller connection parameters. -switches: - nps_dummy_1: - type: dummy - num: 8 - ports: - 1: - name: 'port1' - desc: 'was 1' - 2: - name: 'skye.what.ever' - desc: 'whatever is connected to skye' - 4: - name: 'skyw.what.ever' - desc: 'Something @ skyw' - skye.nps: - type: dummy - ports: - 1: - name: 'skye.pwi' - desc: 'PlaneWavemount Skye' - nps_dummy_3: - type: dummy - ports: - num: 2 - 1: - name: 'skyw.pwi' - desc: 'PlaneWavemount Skyw' - slow: - type: dummy - delay: 2.0 - ports: - num: 1 - 1: - name: 'slow' - fast: - type: dummy - ports: - num: 1 - 1: - name: 'fast' - -timeouts: - switch_connect: 1 - -# Actor configuration for the AMQPActor class -actor: - name: lvmnps - host: localhost - port: 5672 - log_dir: '~/data/logs/lvmnps' diff --git a/tests/test_switches.py b/tests/test_switches.py deleted file mode 100644 index 8599d93..0000000 --- a/tests/test_switches.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# @Author: José Sánchez-Gallego (gallegoj@uw.edu) -# @Date: 2022-05-22 -# @Filename: test_switches.py -# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) - -from __future__ import annotations - -from typing import TYPE_CHECKING - - -if TYPE_CHECKING: - from lvmnps.actor import NPSActor - - -async def test_command_switches(actor: NPSActor): - cmd = await (await actor.invoke_mock_command("switches")) - assert cmd.status.did_succeed - - assert len(cmd.replies) == 2 - assert cmd.replies.get("switches") == [ - "nps_dummy_1", - "skye.nps", - "nps_dummy_3", - "slow", - "fast", - ]