From 4ea2fca6ff2688af873f5aba5d2c620ec8acc61f Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Thu, 4 Aug 2022 08:32:45 -0600 Subject: [PATCH 01/30] Separate web relay into its own class and use it within the WebRelayPreselector. --- src/its_preselector/controlbyweb_web_relay.py | 54 +++++++++++++++++++ .../test/test_web_relay_preselector.py | 10 ++-- src/its_preselector/web_relay.py | 21 ++++++++ src/its_preselector/web_relay_preselector.py | 44 +++------------ 4 files changed, 88 insertions(+), 41 deletions(-) create mode 100644 src/its_preselector/controlbyweb_web_relay.py create mode 100644 src/its_preselector/web_relay.py diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py new file mode 100644 index 0000000..55f0cfe --- /dev/null +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -0,0 +1,54 @@ +from its_preselector.web_relay import WebRelay +import logging +import requests +import xml.etree.ElementTree as ET + +logger = logging.getLogger(__name__) + + +class ControlByWebWebRelay(WebRelay): + + def __init__(self, config): + super().__init__(config) + if 'base_url' in config: + self.base_url = config['base_url'] + + def get_sensor_value(self, sensor_num): + sensor_num_string = str(sensor_num) + response = requests.get(self.base_url) + # Check for X310 xml format first. + sensor_tag = 'sensor' + sensor_num_string + root = ET.fromstring(response.text) + sensor = root.find(sensor_tag) + if sensor is None: + # Didn't find X310 format sensor so check for X410 format. + sensor_tag = 'oneWireSensor' + sensor_num_string + sensor = root.find(sensor_tag) + if sensor is None: + return None + else: + return sensor.text + + def set_state(self, i): + key = str(i) + if key in self.config: + switches = self.config[str(i)].split(',') + if self.base_url and self.base_url != '': + for i in range(len(switches)): + command = self.base_url + '?relay' + switches[i] + logger.debug(command) + response = requests.get(command) + if response.status_code != requests.codes.ok: + raise Exception('Unable to set preselector state. Verify configuration and connectivity.') + else: + raise Exception('base_url is None or blank') + else: + raise Exception("RF path " + key + " configuration does not exist.") + + def healthy(self): + try: + response = requests.get(self.base_url) + return response.status_code == requests.codes.ok + except: + logger.error("Unable to connect to preselector") + return False diff --git a/src/its_preselector/test/test_web_relay_preselector.py b/src/its_preselector/test/test_web_relay_preselector.py index 746b57d..09e1c1a 100644 --- a/src/its_preselector/test/test_web_relay_preselector.py +++ b/src/its_preselector/test/test_web_relay_preselector.py @@ -13,17 +13,20 @@ def setUpClass(cls): file.close() def test_blank_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, {'base_url': '', 'antenna' : '1State=0,2State=0,3State=0,4State=0' }) + preselector = WebRelayPreselector(self.sensor_def, + {'base_url': '', 'antenna': '1State=0,2State=0,3State=0,4State=0'}) with self.assertRaises(Exception): preselector.set_state('antenna') def test_none_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, {'base_url': None, 'antenna' : '1State=0,2State=0,3State=0,4State=0' }) + preselector = WebRelayPreselector(self.sensor_def, + {'base_url': None, 'antenna': '1State=0,2State=0,3State=0,4State=0'}) with self.assertRaises(Exception): preselector.set_state('antenna') def test_invalid_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, {'base_url': 'http://badpreselector.gov', 'antenna' : '1State=0,2State=0,3State=0,4State=0' }) + preselector = WebRelayPreselector(self.sensor_def, {'base_url': 'http://badpreselector.gov', + 'antenna': '1State=0,2State=0,3State=0,4State=0'}) with self.assertRaises(Exception): preselector.set_state('antenna') @@ -32,5 +35,6 @@ def test_healthy_false(self): 'antenna': '1State=0,2State=0,3State=0,4State=0'}) self.assertFalse(preselector.healthy()) + if __name__ == '__main__': unittest.main() diff --git a/src/its_preselector/web_relay.py b/src/its_preselector/web_relay.py new file mode 100644 index 0000000..564f789 --- /dev/null +++ b/src/its_preselector/web_relay.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + + +class WebRelay(ABC): + def __init__(self, config): + self.config = config + + +@abstractmethod +def get_sensor_value(sensor): + pass + + +@abstractmethod +def set_state(self, i): + pass + + +@abstractmethod +def healthy(self): + pass diff --git a/src/its_preselector/web_relay_preselector.py b/src/its_preselector/web_relay_preselector.py index e87ce1d..36d9668 100644 --- a/src/its_preselector/web_relay_preselector.py +++ b/src/its_preselector/web_relay_preselector.py @@ -1,7 +1,7 @@ import logging +from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay from its_preselector.preselector import Preselector -import requests -import xml.etree.ElementTree as ET + logger = logging.getLogger(__name__) @@ -10,45 +10,13 @@ class WebRelayPreselector(Preselector): def __init__(self, sigmf, config): super().__init__(sigmf, config) - if 'base_url' in config: - self.base_url = config['base_url'] + self.web_relay = ControlByWebWebRelay(config) def set_state(self, i): - key = str(i) - if key in self.config: - switches = self.config[str(i)].split(',') - if self.base_url and self.base_url != '': - for i in range(len(switches)): - command = self.base_url + '?relay' + switches[i] - logger.debug(command) - response = requests.get(command) - if response.status_code != requests.codes.ok: - raise Exception('Unable to set preselector state. Verify configuration and connectivity.') - else: - raise Exception('base_url is None or blank') - else: - raise Exception("RF path " + key + " configuration does not exist.") + self.web_relay.set_state(i) def get_sensor_value(self, sensor_num): - sensor_num_string = str(sensor_num) - response = requests.get(self.base_url) - #Check for X310 xml format first. - sensor_tag = 'sensor' + sensor_num_string - root = ET.fromstring(response.text) - sensor = root.find(sensor_tag) - if sensor is None: - #Didn't find X310 format sensor so check for X410 format. - sensor_tag = 'oneWireSensor' + sensor_num_string - sensor = root.find(sensor_tag) - if sensor is None: - return None - else: - return sensor.text + return self.web_relay.get_sensor_value(sensor_num) def healthy(self): - try: - response = requests.get(self.base_url) - return response.status_code == requests.codes.ok - except: - logger.error("Unable to connect to preselector") - return False + return self.web_relay.healthy() From af899a95206f32e516847af599c31a8192128a63 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 9 Aug 2022 13:51:20 -0600 Subject: [PATCH 02/30] Modified config to include name, and control_states and status_states sections to support configurable status requests. Added id and name properties and get_status method to preselector and web_relay interfaces. --- config/config.json | 14 ++- src/its_preselector/controlbyweb_web_relay.py | 89 +++++++++++++++++- src/its_preselector/preselector.py | 19 +++- .../test/test_controlbyweb_web_relay.py | 93 +++++++++++++++++++ .../test/test_web_relay_preselector.py | 29 +++++- src/its_preselector/web_relay.py | 32 +++++-- src/its_preselector/web_relay_preselector.py | 13 +++ 7 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 src/its_preselector/test/test_controlbyweb_web_relay.py diff --git a/config/config.json b/config/config.json index c0748b6..83195b5 100644 --- a/config/config.json +++ b/config/config.json @@ -1,6 +1,14 @@ { + "name": "preselector", "base_url" : "http://192.168.1.2/state.xml", - "noise_diode_on" : "1State=1,2State=1,3State=0,4State=0", - "noise_diode_off" : "1State=1,2State=0,3State=0,4State=0", - "antenna" : "1State=0,2State=0,3State=0,4State=0" + "control_states": { + "noise_diode_on" : "1State=1,2State=1,3State=0,4State=0", + "noise_diode_off" : "1State=1,2State=0,3State=0,4State=0", + "antenna" : "1State=0,2State=0,3State=0,4State=0"}, + "status_states":{ + "noise diode powered" : "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + } + } \ No newline at end of file diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 55f0cfe..12d22a5 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -29,10 +29,9 @@ def get_sensor_value(self, sensor_num): else: return sensor.text - def set_state(self, i): - key = str(i) - if key in self.config: - switches = self.config[str(i)].split(',') + def set_state(self, key): + if key in self.config['control_states']: + switches = self.config['control_states'][str(key)].split(',') if self.base_url and self.base_url != '': for i in range(len(switches)): command = self.base_url + '?relay' + switches[i] @@ -52,3 +51,85 @@ def healthy(self): except: logger.error("Unable to connect to preselector") return False + + @property + def id(self): + return self.base_url + + @property + def name(self): + return self.config['name'] + + def get_status(self): + state = {} + healthy = False + try: + response = self.get_state_xml() + healthy = response.status_code == requests.codes.ok + if healthy: + state_xml = response.text + xml_root = ET.fromstring(state_xml) + + for key, value in self.config['status_states'].items(): + relay_states = value.split(',') + matches = True + for relay_state in relay_states: + matches = matches and self.state_matches(relay_state, xml_root) + state[key] = matches + except: + logger.error('Unable to get status') + state['web_relay_healthy'] = healthy + return state + + def state_matches(self, relay_and_state, xml_root): + relay_state_list = relay_and_state.split('=') + desired_state = relay_state_list[1] + relay_tag = relay_state_list[0] + relay_element = xml_root.find(relay_tag) + if relay_element is None: + raise Exception('Unable to locate ' + relay_tag) + else: + return desired_state == relay_element.text + + def get_state_summary(self, response): + relay_state = '1State=' + self.get_relay_state(response, 'relay1') + \ + ',2State=' + self.get_relay_state(response, 'relay2') + \ + ',3State=' + self.get_relay_state(response, 'relay3') + \ + ',4State=' + self.get_relay_state(response, 'relay4') + return relay_state + + def map_relay_state_to_config(self, relay_state): + for k, value in self.config.items(): + if relay_state == value: + return k + return None + + @staticmethod + def is_enabled(state_xml, relay): + root = ET.fromstring(state_xml) + relay_node = root.find(relay) + if relay_node is None: + raise Exception('Relay ' + relay + ' does not exist.') + else: + relay_state = relay_node.text + if relay_state == '1': + return True + else: + return False + + @staticmethod + def get_relay_state(state_xml, relay): + root = ET.fromstring(state_xml) + relay_node = root.find(relay) + if relay_node is None: + raise Exception('Relay ' + relay + ' does not exist.') + else: + relay_state = relay_node.text + return relay_state + + def get_state_xml(self): + if self.base_url and self.base_url != '': + response = requests.get(self.base_url) + return response + else: + raise Exception('base_url is None or blank') diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index fb5f5b7..5ce5ce5 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -1,4 +1,4 @@ -from abc import ABC, abstractmethod +from abc import ABC, abstractmethod, abstractproperty from its_preselector.rf_path import RfPath from its_preselector.filter import Filter from its_preselector.amplifier import Amplifier @@ -152,6 +152,21 @@ def __get_amplifier(self, amp_id): return None @abstractmethod - def get_sensor_value(sensor): + def get_sensor_value(self, sensor): + pass + + + @property + @abstractmethod + def id(self): + pass + + @property + @abstractmethod + def name(self) -> str: + pass + + @abstractmethod + def get_status(self) -> dict: pass diff --git a/src/its_preselector/test/test_controlbyweb_web_relay.py b/src/its_preselector/test/test_controlbyweb_web_relay.py new file mode 100644 index 0000000..3bef9e2 --- /dev/null +++ b/src/its_preselector/test/test_controlbyweb_web_relay.py @@ -0,0 +1,93 @@ +import unittest +from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay +from requests import codes +from requests import Response +import xml.etree.ElementTree as ET +from unittest.mock import MagicMock +from unittest.mock import PropertyMock + + +class ControlByWebWebRelayTests(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.state = '' \ + '0' \ + '0' \ + '0' \ + '0' \ + '1' \ + '1' \ + '0' \ + '0' \ + '27.6' \ + '0' \ + '102.3' \ + '9160590' \ + '-25200' \ + '00:0C:C8:05:AA:89' \ + '''' + + def test_is_enabled(self): + web_relay = ControlByWebWebRelay({}) + relay1_enabled = web_relay.is_enabled(self.state, 'relay1') + self.assertTrue(relay1_enabled) + relay3_enabled = web_relay.is_enabled(self.state, 'relay3') + self.assertFalse(relay3_enabled) + + # def test_get_relay_summary(self): + # web_relay = ControlByWebWebRelay({"noise_diode_off" : "1State=1,2State=0,3State=0,4State=0"}) + # summary = web_relay.get_state_summary(self.state) + # self.assertEqual(summary, "1State=1,2State=0,3State=0,4State=0") + + def test_state_matches(self): + root = ET.fromstring(self.state) + web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}}) + self.assertTrue(web_relay.state_matches('relay1=1', root)) + + def test_get_state_from_config(self): + root = ET.fromstring(self.state) + web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) + response = Response() + response.status_code = codes.ok + type(response).text = PropertyMock(return_value = self.state) + web_relay.get_state_xml = MagicMock(return_value=response) + states = web_relay.get_status() + self.assertEqual(len(states.keys()), 6) + self.assertTrue(states['noise diode powered']) + self.assertFalse(states['antenna path enabled']) + self.assertFalse(states['measurements']) + self.assertTrue(states['noise diode path enabled']) + self.assertTrue(states['noise on']) + + def test_get_status(self): + web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) + response = Response() + response.status_code = codes.ok + type(response).text = PropertyMock(return_value=self.state) + web_relay.get_state_xml = MagicMock(return_value=response) + states = web_relay.get_status() + self.assertEqual(len(states.keys()), 6) + self.assertTrue(states['noise diode powered']) + self.assertFalse(states['antenna path enabled']) + self.assertFalse(states['measurements']) + self.assertTrue(states['noise diode path enabled']) + self.assertTrue(states['noise on']) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/its_preselector/test/test_web_relay_preselector.py b/src/its_preselector/test/test_web_relay_preselector.py index 09e1c1a..45f78e6 100644 --- a/src/its_preselector/test/test_web_relay_preselector.py +++ b/src/its_preselector/test/test_web_relay_preselector.py @@ -31,10 +31,35 @@ def test_invalid_base_url(self): preselector.set_state('antenna') def test_healthy_false(self): - preselector = WebRelayPreselector(self.sensor_def, {'base_url': 'http://bad_preselector.gov', - 'antenna': '1State=0,2State=0,3State=0,4State=0'}) + preselector =WebRelayPreselector(self.sensor_def, { + 'name': 'preselector', + 'base_url': 'http://bad_preselector.gov', + 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) + self.assertFalse(preselector.healthy()) + def test_get_status_bad_url(self): + preselector = WebRelayPreselector(self.sensor_def, { + 'name': 'preselector', + 'base_url': 'http://bad_preselector.gov', + 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) + status = preselector.get_status() + self.assertFalse(status['web_relay_healthy']) + if __name__ == '__main__': unittest.main() diff --git a/src/its_preselector/web_relay.py b/src/its_preselector/web_relay.py index 564f789..4f0e5a9 100644 --- a/src/its_preselector/web_relay.py +++ b/src/its_preselector/web_relay.py @@ -6,16 +6,30 @@ def __init__(self, config): self.config = config -@abstractmethod -def get_sensor_value(sensor): - pass + @abstractmethod + def get_sensor_value(sensor): + pass -@abstractmethod -def set_state(self, i): - pass + @abstractmethod + def set_state(self, i): + pass -@abstractmethod -def healthy(self): - pass + @abstractmethod + def healthy(self): + pass + + @property + @abstractmethod + def id(self): + pass + + @property + @abstractmethod + def name(self): + pass + + @abstractmethod + def get_status(self): + pass \ No newline at end of file diff --git a/src/its_preselector/web_relay_preselector.py b/src/its_preselector/web_relay_preselector.py index 36d9668..1104586 100644 --- a/src/its_preselector/web_relay_preselector.py +++ b/src/its_preselector/web_relay_preselector.py @@ -20,3 +20,16 @@ def get_sensor_value(self, sensor_num): def healthy(self): return self.web_relay.healthy() + + @property + def id(self): + return self.web_relay.base_url + + @property + def name(self): + return self.web_relay.name + + def get_status(self): + return self.web_relay.get_status() + + From ebfb5218ff5fb425263b657a7439f240758be395 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 14:30:43 -0600 Subject: [PATCH 03/30] Switch to flit backend --- pyproject.toml | 63 ++++++++++++++++++++++++++++++--- requirements.in | 1 - requirements.txt | 16 --------- setup.cfg | 27 -------------- src/its_preselector/__init__.py | 5 +++ 5 files changed, 63 insertions(+), 49 deletions(-) delete mode 100644 requirements.in delete mode 100644 requirements.txt delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index 7173e94..6936dcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,60 @@ [build-system] -requires = [ - "setuptools>=42", - "wheel", - "requests>=2.25.1" +requires = ["flit_core>=3.4,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "its-preselector" +dynamic = ["version", "description"] +readme = "README.md" +requires-python = ">=3.7" +license = { file = "LICENSE.md" } + +authors = [ + { name = "The Institute for Telecommunication Sciences" }, ] -build-backend = "setuptools.build_meta" \ No newline at end of file + +maintainers = [ + { name = "Doug Boulware", email = "dboulware@ntia.gov" }, +] + +keywords = [ + "preselector", "SDR", "NTIA", "web relay", "API", + "radio", "NTIA", "ITS", "telecommunications", "spectrum", +] + +classifiers = [ + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Intended Audience :: Telecommunications Industry", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] + +dependencies = [ + "requests>=2.25.1", +] + +[project.optional-dependencies] +dev = [ + "flit>=3.4,<4", + "pre-commit>=2.20.0", + "pytest>=7.1.2", +] + +[project.urls] +"Repository" = "https://github.com/NTIA/Preselector" +"Bug Tracker" = "https://github.com/NTIA/Preselector/issues" +"NTIA GitHub" = "https://github.com/NTIA" +"ITS Website" = "https://its.ntia.gov" + +[tool.filt.sdist] +exclude = ["doc/"] + +[tool.flit.module] +name = "its_preselector" diff --git a/requirements.in b/requirements.in deleted file mode 100644 index 9e4b84c..0000000 --- a/requirements.in +++ /dev/null @@ -1 +0,0 @@ -requests>=2.25.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index efc9719..0000000 --- a/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.7 -# To update, run: -# -# pip-compile requirements.in -# -certifi==2021.10.8 - # via requests -charset-normalizer==2.0.12 - # via requests -idna==3.3 - # via requests -requests==2.27.1 - # via -r requirements.in -urllib3==1.26.8 - # via requests diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 24a78b5..0000000 --- a/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[metadata] -name = its-preselector -version = 1.0.0 -author = ITS -author_email = dboulware@ntia.gov -description = A package to control the ITS web relay based preselector -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/ntia/Preselector -project_urls = - Bug Tracker = https://github.com/ntia/Preselector/issues -classifiers = - Programming Language :: Python :: 3 - License :: OSI Approved :: MIT License - Operating System :: OS Independent - -[options] -package_dir = - = src -packages = - its_preselector -python_requires = >=3.7 -install_requires = - requests>=2.25.1 - -[options.packages.find] -where = src \ No newline at end of file diff --git a/src/its_preselector/__init__.py b/src/its_preselector/__init__.py index e69de29..063a8bc 100644 --- a/src/its_preselector/__init__.py +++ b/src/its_preselector/__init__.py @@ -0,0 +1,5 @@ +"""A package to control the ITS web relay-based preselector + +Refer to the README for more detailed usage information. +""" +__version__ = "1.0.0" From c868d62ab5037bf64c1c8888e5b8f7fb72efa283 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 14:31:05 -0600 Subject: [PATCH 04/30] README cleanup + add badges --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ecd189d..cc58c85 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # NTIA/ITS Preselector API +![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/NTIA/Preselector?display_name=tag&sort=semver) +![GitHub all releases](https://img.shields.io/github/downloads/NTIA/Preselector/total) +![GitHub issues](https://img.shields.io/github/issues/NTIA/Preselector) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + This repository provides a general software API to control preselectors regardless of their components and control mechanisms. @@ -15,16 +20,15 @@ This software will grow over time to support additional components and control m ## Introduction - A preselector is a device, connected between an antenna and a signal analyzer, designed to - improve the RF performance and capability of a sensor. As illustrated in the diagram below, - it may include a variety of components, e.g., filters, amplifiers, calibration sources, and - switches. An example preselector is shown in Figure 1. Just as the components within a preselector - may change, so too may the way in which the switching is controlled. +A preselector is a device, connected between an antenna and a signal analyzer, designed to +improve the RF performance and capability of a sensor. As illustrated in the diagram below, +it may include a variety of components, e.g., filters, amplifiers, calibration sources, and +switches. An example preselector is shown in Figure 1. Just as the components within a preselector +may change, so too may the way in which the switching is controlled. ![Preselector Diagram](/docs/img/preselector.png)

Figure.1 - Example Preselector

- ## Installation To install this Python package, clone the repository and enter the directory of the project in From 54c1ee6b07376a8588914c981706cb35f268c72f Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 14:31:29 -0600 Subject: [PATCH 05/30] Moved tests outside of src --- .../null_preselector.sigmf-meta | 0 .../test => tests}/sensor_definition.json | 0 .../test => tests}/test_amplifier.py | 6 +- .../test => tests}/test_cal_source.py | 7 +- .../test_controlbyweb_web_relay.py | 0 .../test => tests}/test_filter.py | 7 +- .../test => tests}/test_metadata.sigmf-meta | 0 .../test => tests}/test_preselector.py | 8 +- .../test => tests}/test_rfpaths.py | 81 ++++++++++--------- .../test_web_relay_preselector.py | 5 +- 10 files changed, 61 insertions(+), 53 deletions(-) rename {src/its_preselector/test => tests}/null_preselector.sigmf-meta (100%) rename {src/its_preselector/test => tests}/sensor_definition.json (100%) rename {src/its_preselector/test => tests}/test_amplifier.py (87%) rename {src/its_preselector/test => tests}/test_cal_source.py (87%) rename {src/its_preselector/test => tests}/test_controlbyweb_web_relay.py (100%) rename {src/its_preselector/test => tests}/test_filter.py (88%) rename {src/its_preselector/test => tests}/test_metadata.sigmf-meta (100%) rename {src/its_preselector/test => tests}/test_preselector.py (92%) rename {src/its_preselector/test => tests}/test_rfpaths.py (86%) rename {src/its_preselector/test => tests}/test_web_relay_preselector.py (95%) diff --git a/src/its_preselector/test/null_preselector.sigmf-meta b/tests/null_preselector.sigmf-meta similarity index 100% rename from src/its_preselector/test/null_preselector.sigmf-meta rename to tests/null_preselector.sigmf-meta diff --git a/src/its_preselector/test/sensor_definition.json b/tests/sensor_definition.json similarity index 100% rename from src/its_preselector/test/sensor_definition.json rename to tests/sensor_definition.json diff --git a/src/its_preselector/test/test_amplifier.py b/tests/test_amplifier.py similarity index 87% rename from src/its_preselector/test/test_amplifier.py rename to tests/test_amplifier.py index a4f6802..cc474fe 100644 --- a/src/its_preselector/test/test_amplifier.py +++ b/tests/test_amplifier.py @@ -1,17 +1,19 @@ import unittest from its_preselector.web_relay_preselector import WebRelayPreselector import json +from pathlib import Path class TestAmplifier(unittest.TestCase): @classmethod def setUpClass(cls): - file = open('test_metadata.sigmf-meta') + fpath = Path(__file__).parent.resolve() + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open('null_preselector.sigmf-meta') + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) diff --git a/src/its_preselector/test/test_cal_source.py b/tests/test_cal_source.py similarity index 87% rename from src/its_preselector/test/test_cal_source.py rename to tests/test_cal_source.py index d1fb329..79dd3f3 100644 --- a/src/its_preselector/test/test_cal_source.py +++ b/tests/test_cal_source.py @@ -1,17 +1,18 @@ import unittest from its_preselector.web_relay_preselector import WebRelayPreselector import json - +from pathlib import Path class TestCalSource(unittest.TestCase): @classmethod def setUpClass(cls): - file = open('test_metadata.sigmf-meta') + fpath = Path(__file__).parent.resolve() + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open('null_preselector.sigmf-meta') + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) diff --git a/src/its_preselector/test/test_controlbyweb_web_relay.py b/tests/test_controlbyweb_web_relay.py similarity index 100% rename from src/its_preselector/test/test_controlbyweb_web_relay.py rename to tests/test_controlbyweb_web_relay.py diff --git a/src/its_preselector/test/test_filter.py b/tests/test_filter.py similarity index 88% rename from src/its_preselector/test/test_filter.py rename to tests/test_filter.py index bb6100c..93b1219 100644 --- a/src/its_preselector/test/test_filter.py +++ b/tests/test_filter.py @@ -1,17 +1,18 @@ import unittest from its_preselector.web_relay_preselector import WebRelayPreselector import json - +from pathlib import Path class TestFilter(unittest.TestCase): @classmethod def setUpClass(cls): - file = open('test_metadata.sigmf-meta') + fpath = Path(__file__).parent.resolve() + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open('null_preselector.sigmf-meta') + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) diff --git a/src/its_preselector/test/test_metadata.sigmf-meta b/tests/test_metadata.sigmf-meta similarity index 100% rename from src/its_preselector/test/test_metadata.sigmf-meta rename to tests/test_metadata.sigmf-meta diff --git a/src/its_preselector/test/test_preselector.py b/tests/test_preselector.py similarity index 92% rename from src/its_preselector/test/test_preselector.py rename to tests/test_preselector.py index ca812f7..61c0e7d 100644 --- a/src/its_preselector/test/test_preselector.py +++ b/tests/test_preselector.py @@ -1,21 +1,23 @@ import unittest from its_preselector.web_relay_preselector import WebRelayPreselector import json +from pathlib import Path class TestWebRelayPreselector(unittest.TestCase): @classmethod def setUpClass(cls): - file = open('test_metadata.sigmf-meta') + fpath = Path(__file__).parent.resolve() + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open('null_preselector.sigmf-meta') + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) - with open('sensor_definition.json', 'r') as f: + with open(fpath / 'sensor_definition.json', 'r') as f: sensor_def = json.load(f) cls.scos_preselector = WebRelayPreselector(sensor_def, {}) diff --git a/src/its_preselector/test/test_rfpaths.py b/tests/test_rfpaths.py similarity index 86% rename from src/its_preselector/test/test_rfpaths.py rename to tests/test_rfpaths.py index ab22d2b..9a70745 100644 --- a/src/its_preselector/test/test_rfpaths.py +++ b/tests/test_rfpaths.py @@ -1,40 +1,41 @@ -import json -import unittest -from its_preselector.web_relay_preselector import WebRelayPreselector - - -class TestRFPaths(unittest.TestCase): - - @classmethod - def setUpClass(cls): - file = open('test_metadata.sigmf-meta') - sensor_def = json.load(file) - file.close() - cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open('null_preselector.sigmf-meta') - null_def = json.load(null_file) - null_file.close() - cls.empty_preselector = WebRelayPreselector(null_def, {}) - - def test_number_valid_paths(self): - self.assertEqual(2, len(self.preselector.rf_paths)) - - def test_empty_paths(self): - self.assertIsNotNone(self.empty_preselector.rf_paths) - self.assertEqual(0, len(self.empty_preselector.rf_paths)) - - def test_name(self): - self.assertEqual('noise_diode_on', self.preselector.rf_paths[0].name) - - def test_cal_source_id(self): - self.assertEqual('SG53400067', self.preselector.rf_paths[0].cal_source_id) - - def test_filter_id(self): - self.assertEqual('13FV40, SN 9', self.preselector.rf_paths[0].filter_id) - - def test_amplifier_id(self): - self.assertEqual('1502150', self.preselector.rf_paths[0].amplifier_id) - - -if __name__ == '__main__': - unittest.main() +import json +import unittest +from its_preselector.web_relay_preselector import WebRelayPreselector +from pathlib import Path + +class TestRFPaths(unittest.TestCase): + + @classmethod + def setUpClass(cls): + fpath = Path(__file__).parent.resolve() + file = open(fpath / 'test_metadata.sigmf-meta') + sensor_def = json.load(file) + file.close() + cls.preselector = WebRelayPreselector(sensor_def, {}) + null_file = open(fpath / 'null_preselector.sigmf-meta') + null_def = json.load(null_file) + null_file.close() + cls.empty_preselector = WebRelayPreselector(null_def, {}) + + def test_number_valid_paths(self): + self.assertEqual(2, len(self.preselector.rf_paths)) + + def test_empty_paths(self): + self.assertIsNotNone(self.empty_preselector.rf_paths) + self.assertEqual(0, len(self.empty_preselector.rf_paths)) + + def test_name(self): + self.assertEqual('noise_diode_on', self.preselector.rf_paths[0].name) + + def test_cal_source_id(self): + self.assertEqual('SG53400067', self.preselector.rf_paths[0].cal_source_id) + + def test_filter_id(self): + self.assertEqual('13FV40, SN 9', self.preselector.rf_paths[0].filter_id) + + def test_amplifier_id(self): + self.assertEqual('1502150', self.preselector.rf_paths[0].amplifier_id) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/its_preselector/test/test_web_relay_preselector.py b/tests/test_web_relay_preselector.py similarity index 95% rename from src/its_preselector/test/test_web_relay_preselector.py rename to tests/test_web_relay_preselector.py index 45f78e6..ca94261 100644 --- a/src/its_preselector/test/test_web_relay_preselector.py +++ b/tests/test_web_relay_preselector.py @@ -1,6 +1,6 @@ import unittest import json - +from pathlib import Path from its_preselector.web_relay_preselector import WebRelayPreselector @@ -8,7 +8,8 @@ class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): - file = open('test_metadata.sigmf-meta') + fpath = Path(__file__).parent.resolve() + file = open(fpath / 'test_metadata.sigmf-meta') cls.sensor_def = json.load(file) file.close() From 4b6487006f95b4ebce494fbc42aba3ecc5afdb4d Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 14:31:49 -0600 Subject: [PATCH 06/30] Added pre-commit configuration --- .markdownlint.yaml | 3 +++ .pre-commit-config.yaml | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .markdownlint.yaml create mode 100644 .pre-commit-config.yaml diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..035730e --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,3 @@ +default: true +MD013: + line_length: 88 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..67568c2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +default_language_version: + python: python3.7 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-ast + types: [file, python] + - id: check-case-conflict + - id: check-docstring-first + types: [file, python] + - id: check-merge-conflict + - id: check-yaml + types: [file, yaml] + - id: debug-statements + types: [file, python] + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/asottile/pyupgrade + rev: v2.34.0 + hooks: + - id: pyupgrade + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + name: isort (python) + types: [file, python] + args: ["--profile", "black", "--filter-files", "--gitignore"] + - repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black + types: [file, python] + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.31.1 + hooks: + - id: markdownlint + types: [file, markdown] + exclude: GitHubRepoPublicReleaseApproval.md|LICENSE.md From 5d5e155b73a84bc85c9862d988f3d7108ff049b2 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 14:33:40 -0600 Subject: [PATCH 07/30] Fix typo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6936dcc..0057345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dev = [ "NTIA GitHub" = "https://github.com/NTIA" "ITS Website" = "https://its.ntia.gov" -[tool.filt.sdist] +[tool.flit.sdist] exclude = ["doc/"] [tool.flit.module] From c4b9fb71349d689f11a7e05ed5bb03c8f64e8605 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 14:34:20 -0600 Subject: [PATCH 08/30] Add pytest-cov dev dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0057345..4ea3b99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dev = [ "flit>=3.4,<4", "pre-commit>=2.20.0", "pytest>=7.1.2", + "pytest-cov>=3.0.0", ] [project.urls] From d917f7b1ae85d38d70905c35443728de52c4e4f6 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 14:38:18 -0600 Subject: [PATCH 09/30] Include docs in sdist --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ea3b99..60a3679 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,8 +54,5 @@ dev = [ "NTIA GitHub" = "https://github.com/NTIA" "ITS Website" = "https://its.ntia.gov" -[tool.flit.sdist] -exclude = ["doc/"] - [tool.flit.module] name = "its_preselector" From a9d1d3e5ead42a5d56de7a0d1c298dfb734dffc3 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 16:35:25 -0600 Subject: [PATCH 10/30] Update README --- README.md | 94 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index cc58c85..6130b8f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ See below for additional details on using the `WebRelayPreselector`. This software will grow over time to support additional components and control mechanisms. +## Table of Contents + +- Introduction +- Usage +- Development +- License +- Contact + ## Introduction A preselector is a device, connected between an antenna and a signal analyzer, designed to @@ -29,29 +37,26 @@ may change, so too may the way in which the switching is controlled. ![Preselector Diagram](/docs/img/preselector.png)

Figure.1 - Example Preselector

-## Installation +## Usage -To install this Python package, clone the repository and enter the directory of the project in -the command line (should be the same location as `setup.cfg`). Execute the following commands -depending on your OS (you may have to adjust for your version of Python): +To install this Python package, clone the repository and enter the directory of the +project in the command line. Execute the following commands depending on your OS (you may +have to adjust for your version of Python): ```bash # Windows -py –m build -py -m pip install dist/its-preselector-2.0.1.tar.gz +py -m pip install . # Linux -python3 -m build -python3 –m pip install dist/its-preselector-2.0.1.tar.gz - +python3 –m pip install . ``` -## `WebRelayPreselector` Configuration +### `WebRelayPreselector` Configuration The `WebRelayPreselector` requires a [SigMF metadata file](https://Github.com/NTIA/sigmf-ns-ntia) -that describes the sensor preselector and a config file to describe the x310 settings for the RF -paths specified in the metadata and for any other desired sources. Below is an example config file -for the `WebRelayPreselector` to describe how it works: +that describes the sensor preselector and a config file to describe the x310 settings for +the RF paths specified in the metadata and for any other desired sources. Below is an +example config file for the `WebRelayPreselector` to describe how it works: ```json { @@ -62,22 +67,23 @@ for the `WebRelayPreselector` to describe how it works: } ``` -The `base_url` key is the only required key for the `WebRelayPreselector` and should map to the -base URL to interact with the WebRelay (see [https://www.controlbyweb.com/x310](https://www.controlbyweb.com/x310) -for more info). The other keys should correspond to RF paths documented in the SigMF metadata. -Each of the entries in the config provide mappings to the associated web relay input states and -every RFPath defined in the sensor definition json file should have an entry in the preselector -config. The keys in the dictionary may use the name of the RFPath or the index of the RFPath in -the RFPaths array. +The `base_url` key is the only required key for the `WebRelayPreselector` and should map +to the base URL to interact with the WebRelay (see +[https://www.controlbyweb.com/x310](https://www.controlbyweb.com/x310) for more info). +The other keys should correspond to RF paths documented in the SigMF metadata. Each of the +entries in the config provide mappings to the associated web relay input states and every +RFPath defined in the sensor definition json file should have an entry in the preselector +config. The keys in the dictionary may use the name of the RFPath or the index of the RFPath +in the RFPaths array. In this example, there are `noise_diode_on` and `noise_diode_off` keys to correspond to the -preselector paths to turn the noise diode on and off, and an antenna key to indicate the web -relay states to connect to the antenna. +preselector paths to turn the noise diode on and off, and an antenna key to indicate the +web relay states to connect to the antenna. -Note: with this example configuration, you would have to set the path by the name of the source -rather than the index in the `rf_paths` array. +Note: with this example configuration, you would have to set the path by the name of the +source rather than the index in the `rf_paths` array. -## `WebRelayPreselector` Initialization +### `WebRelayPreselector` Initialization ```python import json @@ -94,14 +100,14 @@ preselector = WebRelayPreselector(sensor_def, preselector_config) preselector.set_state('antenna') ``` -## Preselector Interactions +### Preselector Interactions -### Access instance properties +#### Access instance properties - `preselector.amplifiers[0].gain` - ... -### Helper methods +#### Helper methods - `preselector.get_amplifier_gain(rf_path_index)` - `preselector.get_amplifier_noise_figure(rf_path_index)` @@ -110,13 +116,41 @@ preselector.set_state('antenna') - `preselector.get_frequency_low_stopband(rf_path_index)` - `preselector.get_frequency_high_stopband(rf_path_index)` -### Control +#### Control - `preselector.set_state(rf_path_name)` +## Development + +Set up a development environment using a tool like [Conda](https://docs.conda.io/en/latest/) +or [venv](https://docs.python.org/3/library/venv.html#module-venv), with `python>=3.7`. Then, +from the cloned directory, install the development dependencies by running: + +```bash +pip install .[dev] +``` + +This will install the project itself, along with development dependencies for pre-commit +hooks, building distributions, and running tests. Set up pre-commit, which runs auto-formatting +and code-checking automatically when you make a commit, by running: + +```bash +pre-commit install +``` + +### Building New Releases + +This project uses [flit](https://github.com/pypa/flit) as a backend. To build a new release +(both wheel and sdist/tarball), first update the version number in +[`src/its_preselector/__init__.py`], then run: + +```bash +flit build +``` + ## License -See [LICENSE](LICENSE.md). +See [LICENSE](LICENSE.md) ## Contact From d66ab1a8246b662a0126438f8e4154ee7858f816 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 16:50:04 -0600 Subject: [PATCH 11/30] Update example config --- .markdownlint.yaml | 3 +++ README.md | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 035730e..3635065 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,3 +1,6 @@ default: true MD013: line_length: 88 +MD033: + allowed_elements: + - "figcaption" diff --git a/README.md b/README.md index 6130b8f..4894758 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A simple `set_state` method allows users to specify the state of the preselector key specified in the preselector config. Different switching control mechanisms are supported by extending the base `Preselector` class. Currently, this repository provides an implementation for a `WebRelayPreselector` that includes an [x310 WebRelay](https://www.controlbyweb.com/x310/). -See below for additional details on using the `WebRelayPreselector`. +See below for additional details on using the `WebRelayPreselector`. This software will grow over time to support additional components and control mechanisms. @@ -35,7 +35,7 @@ switches. An example preselector is shown in Figure 1. Just as the components wi may change, so too may the way in which the switching is controlled. ![Preselector Diagram](/docs/img/preselector.png) -

Figure.1 - Example Preselector

+
Figure 1: Block diagram showing an example RF measurement system with a preselector.
## Usage @@ -60,10 +60,18 @@ example config file for the `WebRelayPreselector` to describe how it works: ```json { - "base_url" : "http://192.168.130.32/state.xml", - "noise_diode_on" : "1State=1,2State=1,3State=0,4State=0", - "noise_diode_off" : "1State=0,2State=1,3State=0,4State=0", - "antenna" : "1State=0,2State=0,3State=0,4State=0" + "name": "preselector", + "base_url" : "http://192.168.1.2/state.xml", + "control_states": { + "noise_diode_on" : "1State=1,2State=1,3State=0,4State=0", + "noise_diode_off" : "1State=1,2State=0,3State=0,4State=0", + "antenna" : "1State=0,2State=0,3State=0,4State=0" + }, + "status_states": { + "noise diode powered" : "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + } } ``` From b2e89ed7e192dcf2f980b8f92d08bcad960818ac Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Wed, 10 Aug 2022 16:59:19 -0600 Subject: [PATCH 12/30] pre-commit run on all files --- .gitignore | 2 +- LICENSE.md | 5 +- config/config.json | 2 +- config/metadata.sigmf-meta | 2 +- src/its_preselector/amplifier.py | 17 ++- src/its_preselector/cal_source.py | 13 +- src/its_preselector/controlbyweb_web_relay.py | 67 +++++---- src/its_preselector/filter.py | 21 ++- src/its_preselector/hardware_spec.py | 19 ++- src/its_preselector/preselector.py | 50 ++++--- src/its_preselector/rf_path.py | 18 ++- src/its_preselector/web_relay.py | 5 +- src/its_preselector/web_relay_preselector.py | 5 +- tests/null_preselector.sigmf-meta | 2 +- tests/test_amplifier.py | 17 ++- tests/test_cal_source.py | 19 ++- tests/test_controlbyweb_web_relay.py | 128 ++++++++++-------- tests/test_filter.py | 20 +-- tests/test_metadata.sigmf-meta | 2 +- tests/test_preselector.py | 36 +++-- tests/test_rfpaths.py | 19 +-- tests/test_web_relay_preselector.py | 93 ++++++++----- 22 files changed, 316 insertions(+), 246 deletions(-) diff --git a/.gitignore b/.gitignore index b234207..13d04e1 100644 --- a/.gitignore +++ b/.gitignore @@ -147,4 +147,4 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties -fabric.properties \ No newline at end of file +fabric.properties diff --git a/LICENSE.md b/LICENSE.md index 12746a1..fd7fe11 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,13 +2,12 @@ SOFTWARE DISCLAIMER / RELEASE This software was developed by employees of the National Telecommunications and Information Administration (NTIA), an agency of the Federal Government and is provided to you as a public service. Pursuant to Title 15 United States Code Section 105, works of NTIA employees are not subject to copyright protection within the United States. -The software is provided by NTIA “AS IS.” NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does not warrant or make any representations regarding the use of the software or the results thereof, including but not limited to the correctness, accuracy, reliability or usefulness of the software. +The software is provided by NTIA “AS IS.” NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does not warrant or make any representations regarding the use of the software or the results thereof, including but not limited to the correctness, accuracy, reliability or usefulness of the software. To the extent that NTIA holds rights in countries other than the United States, you are hereby granted the non-exclusive irrevocable and unconditional right to print, publish, prepare derivative works and distribute the NTIA software, in any medium, or authorize others to do so on your behalf, on a royalty-free basis throughout the World. You may improve, modify, and create derivative works of the software or any portion of the software, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the software and should note the date and nature of any such change. -You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. +You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. Please provide appropriate acknowledgments of NTIA’s creation of the software in any copies or derivative works of this software. - diff --git a/config/config.json b/config/config.json index 83195b5..405c38b 100644 --- a/config/config.json +++ b/config/config.json @@ -11,4 +11,4 @@ "noise diode path enabled": "relay1=1", } -} \ No newline at end of file +} diff --git a/config/metadata.sigmf-meta b/config/metadata.sigmf-meta index 42c7c29..7c2c2dd 100644 --- a/config/metadata.sigmf-meta +++ b/config/metadata.sigmf-meta @@ -164,4 +164,4 @@ "ntia-core:azimuth_angle" : 90.0, "ntia-core:elevation_angle" : 0.0 } ] -} \ No newline at end of file +} diff --git a/src/its_preselector/amplifier.py b/src/its_preselector/amplifier.py index d3a94ea..b157ca5 100644 --- a/src/its_preselector/amplifier.py +++ b/src/its_preselector/amplifier.py @@ -2,17 +2,16 @@ class Amplifier: - def __init__(self, meta): self.amplifier_spec = None self.gain = None self.noise_figure = None self.max_power = None - if 'amplifier_spec' in meta: - self.amplifier_spec = HardwareSpec(meta['amplifier_spec']) - if 'gain' in meta: - self.gain = meta['gain'] - if 'noise_figure' in meta: - self.noise_figure = meta['noise_figure'] - if 'max_power' in meta: - self.max_power = meta['max_power'] + if "amplifier_spec" in meta: + self.amplifier_spec = HardwareSpec(meta["amplifier_spec"]) + if "gain" in meta: + self.gain = meta["gain"] + if "noise_figure" in meta: + self.noise_figure = meta["noise_figure"] + if "max_power" in meta: + self.max_power = meta["max_power"] diff --git a/src/its_preselector/cal_source.py b/src/its_preselector/cal_source.py index 028ce4d..35e8cc3 100644 --- a/src/its_preselector/cal_source.py +++ b/src/its_preselector/cal_source.py @@ -2,14 +2,13 @@ class CalSource: - def __init__(self, props): self.cal_source_spec = None self.type = None self.enr = None - if 'cal_source_spec' in props: - self.cal_source_spec = HardwareSpec(props['cal_source_spec']) - if 'type' in props: - self.type = props['type'] - if 'enr' in props: - self.enr = props['enr'] + if "cal_source_spec" in props: + self.cal_source_spec = HardwareSpec(props["cal_source_spec"]) + if "type" in props: + self.type = props["type"] + if "enr" in props: + self.enr = props["enr"] diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 12d22a5..b8254a7 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -1,28 +1,29 @@ -from its_preselector.web_relay import WebRelay import logging -import requests import xml.etree.ElementTree as ET +import requests + +from its_preselector.web_relay import WebRelay + logger = logging.getLogger(__name__) class ControlByWebWebRelay(WebRelay): - def __init__(self, config): super().__init__(config) - if 'base_url' in config: - self.base_url = config['base_url'] + if "base_url" in config: + self.base_url = config["base_url"] def get_sensor_value(self, sensor_num): sensor_num_string = str(sensor_num) response = requests.get(self.base_url) # Check for X310 xml format first. - sensor_tag = 'sensor' + sensor_num_string + sensor_tag = "sensor" + sensor_num_string root = ET.fromstring(response.text) sensor = root.find(sensor_tag) if sensor is None: # Didn't find X310 format sensor so check for X410 format. - sensor_tag = 'oneWireSensor' + sensor_num_string + sensor_tag = "oneWireSensor" + sensor_num_string sensor = root.find(sensor_tag) if sensor is None: return None @@ -30,17 +31,19 @@ def get_sensor_value(self, sensor_num): return sensor.text def set_state(self, key): - if key in self.config['control_states']: - switches = self.config['control_states'][str(key)].split(',') - if self.base_url and self.base_url != '': + if key in self.config["control_states"]: + switches = self.config["control_states"][str(key)].split(",") + if self.base_url and self.base_url != "": for i in range(len(switches)): - command = self.base_url + '?relay' + switches[i] + command = self.base_url + "?relay" + switches[i] logger.debug(command) response = requests.get(command) if response.status_code != requests.codes.ok: - raise Exception('Unable to set preselector state. Verify configuration and connectivity.') + raise Exception( + "Unable to set preselector state. Verify configuration and connectivity." + ) else: - raise Exception('base_url is None or blank') + raise Exception("base_url is None or blank") else: raise Exception("RF path " + key + " configuration does not exist.") @@ -58,7 +61,7 @@ def id(self): @property def name(self): - return self.config['name'] + return self.config["name"] def get_status(self): state = {} @@ -70,32 +73,38 @@ def get_status(self): state_xml = response.text xml_root = ET.fromstring(state_xml) - for key, value in self.config['status_states'].items(): - relay_states = value.split(',') + for key, value in self.config["status_states"].items(): + relay_states = value.split(",") matches = True for relay_state in relay_states: matches = matches and self.state_matches(relay_state, xml_root) state[key] = matches except: - logger.error('Unable to get status') - state['web_relay_healthy'] = healthy + logger.error("Unable to get status") + state["web_relay_healthy"] = healthy return state def state_matches(self, relay_and_state, xml_root): - relay_state_list = relay_and_state.split('=') + relay_state_list = relay_and_state.split("=") desired_state = relay_state_list[1] relay_tag = relay_state_list[0] relay_element = xml_root.find(relay_tag) if relay_element is None: - raise Exception('Unable to locate ' + relay_tag) + raise Exception("Unable to locate " + relay_tag) else: return desired_state == relay_element.text def get_state_summary(self, response): - relay_state = '1State=' + self.get_relay_state(response, 'relay1') + \ - ',2State=' + self.get_relay_state(response, 'relay2') + \ - ',3State=' + self.get_relay_state(response, 'relay3') + \ - ',4State=' + self.get_relay_state(response, 'relay4') + relay_state = ( + "1State=" + + self.get_relay_state(response, "relay1") + + ",2State=" + + self.get_relay_state(response, "relay2") + + ",3State=" + + self.get_relay_state(response, "relay3") + + ",4State=" + + self.get_relay_state(response, "relay4") + ) return relay_state def map_relay_state_to_config(self, relay_state): @@ -109,10 +118,10 @@ def is_enabled(state_xml, relay): root = ET.fromstring(state_xml) relay_node = root.find(relay) if relay_node is None: - raise Exception('Relay ' + relay + ' does not exist.') + raise Exception("Relay " + relay + " does not exist.") else: relay_state = relay_node.text - if relay_state == '1': + if relay_state == "1": return True else: return False @@ -122,14 +131,14 @@ def get_relay_state(state_xml, relay): root = ET.fromstring(state_xml) relay_node = root.find(relay) if relay_node is None: - raise Exception('Relay ' + relay + ' does not exist.') + raise Exception("Relay " + relay + " does not exist.") else: relay_state = relay_node.text return relay_state def get_state_xml(self): - if self.base_url and self.base_url != '': + if self.base_url and self.base_url != "": response = requests.get(self.base_url) return response else: - raise Exception('base_url is None or blank') + raise Exception("base_url is None or blank") diff --git a/src/its_preselector/filter.py b/src/its_preselector/filter.py index 966c093..24b566e 100644 --- a/src/its_preselector/filter.py +++ b/src/its_preselector/filter.py @@ -2,20 +2,19 @@ class Filter: - def __init__(self, props): self.filter_spec = None self.frequency_low_passband = None self.frequency_high_passband = None self.frequency_low_stopband = None self.frequency_high_stopband = None - if 'filter_spec' in props: - self.filter_spec = HardwareSpec(props['filter_spec']) - if 'frequency_low_passband' in props: - self.frequency_low_passband = props['frequency_low_passband'] - if 'frequency_high_passband' in props: - self.frequency_high_passband = props['frequency_high_passband'] - if 'frequency_low_stopband' in props: - self.frequency_low_stopband = props['frequency_low_stopband'] - if 'frequency_high_stopband' in props: - self.frequency_high_stopband = props['frequency_high_stopband'] + if "filter_spec" in props: + self.filter_spec = HardwareSpec(props["filter_spec"]) + if "frequency_low_passband" in props: + self.frequency_low_passband = props["frequency_low_passband"] + if "frequency_high_passband" in props: + self.frequency_high_passband = props["frequency_high_passband"] + if "frequency_low_stopband" in props: + self.frequency_low_stopband = props["frequency_low_stopband"] + if "frequency_high_stopband" in props: + self.frequency_high_stopband = props["frequency_high_stopband"] diff --git a/src/its_preselector/hardware_spec.py b/src/its_preselector/hardware_spec.py index caf35d9..1237998 100644 --- a/src/its_preselector/hardware_spec.py +++ b/src/its_preselector/hardware_spec.py @@ -1,15 +1,14 @@ class HardwareSpec: - def __init__(self, props): - self.id=None + self.id = None self.model = None self.version = None self.supplemental_information = None - if 'id' in props: - self.id = props['id'] - if 'model' in props: - self.model = props['model'] - if 'supplemental_information' in props: - self.supplemental_information = props['supplemental_information'] - if 'version' in props: - self.version = props['version'] + if "id" in props: + self.id = props["id"] + if "model" in props: + self.model = props["model"] + if "supplemental_information" in props: + self.supplemental_information = props["supplemental_information"] + if "version" in props: + self.version = props["version"] diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index 5ce5ce5..9d089b4 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -1,13 +1,13 @@ from abc import ABC, abstractmethod, abstractproperty -from its_preselector.rf_path import RfPath -from its_preselector.filter import Filter + from its_preselector.amplifier import Amplifier from its_preselector.cal_source import CalSource +from its_preselector.filter import Filter from its_preselector.hardware_spec import HardwareSpec +from its_preselector.rf_path import RfPath class Preselector(ABC): - def __init__(self, sigmf, config): self.amplifiers = [] self.rf_paths = [] @@ -16,44 +16,56 @@ def __init__(self, sigmf, config): self.preselector_spec = [] self.config = config try: - if 'global' in sigmf: - self.__set_filters(sigmf['global']['ntia-sensor:sensor']['preselector']['filters']) + if "global" in sigmf: + self.__set_filters( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["filters"] + ) else: - self.__set_filters(sigmf['preselector']['filters']) + self.__set_filters(sigmf["preselector"]["filters"]) except KeyError: pass try: - if 'global' in sigmf: - self.__set_amplifiers(sigmf['global']['ntia-sensor:sensor']['preselector']['amplifiers']) + if "global" in sigmf: + self.__set_amplifiers( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["amplifiers"] + ) else: - self.__set_amplifiers(sigmf['preselector']['amplifiers']) + self.__set_amplifiers(sigmf["preselector"]["amplifiers"]) except KeyError: pass try: - if 'global' in sigmf: - self.__get_rf_paths(sigmf['global']['ntia-sensor:sensor']['preselector']['rf_paths']) + if "global" in sigmf: + self.__get_rf_paths( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["rf_paths"] + ) else: - self.__get_rf_paths(sigmf['preselector']['rf_paths']) + self.__get_rf_paths(sigmf["preselector"]["rf_paths"]) except KeyError: pass try: - if 'global' in sigmf: - self.__set_cal_sources(sigmf['global']['ntia-sensor:sensor']['preselector']['cal_sources']) + if "global" in sigmf: + self.__set_cal_sources( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["cal_sources"] + ) else: - self.__set_cal_sources(sigmf['preselector']['cal_sources']) + self.__set_cal_sources(sigmf["preselector"]["cal_sources"]) except KeyError: pass try: - if 'global' in sigmf: + if "global" in sigmf: self.preselector_spec = HardwareSpec( - sigmf['global']['ntia-sensor:sensor']['preselector']['preselector_spec']) + sigmf["global"]["ntia-sensor:sensor"]["preselector"][ + "preselector_spec" + ] + ) else: self.preselector_spec = HardwareSpec( - sigmf['preselector']['preselector_spec']) + sigmf["preselector"]["preselector_spec"] + ) except KeyError: pass @@ -155,7 +167,6 @@ def __get_amplifier(self, amp_id): def get_sensor_value(self, sensor): pass - @property @abstractmethod def id(self): @@ -169,4 +180,3 @@ def name(self) -> str: @abstractmethod def get_status(self) -> dict: pass - diff --git a/src/its_preselector/rf_path.py b/src/its_preselector/rf_path.py index 2bad120..bec38a4 100644 --- a/src/its_preselector/rf_path.py +++ b/src/its_preselector/rf_path.py @@ -1,13 +1,11 @@ class RfPath: - def __init__(self, props): self.name = None - if 'cal_source_id' in props: - self.cal_source_id = props['cal_source_id'] - if 'filter_id' in props: - self.filter_id = props['filter_id'] - if 'amplifier_id' in props: - self.amplifier_id = props['amplifier_id'] - if 'name' in props: - self.name = props['name'] - + if "cal_source_id" in props: + self.cal_source_id = props["cal_source_id"] + if "filter_id" in props: + self.filter_id = props["filter_id"] + if "amplifier_id" in props: + self.amplifier_id = props["amplifier_id"] + if "name" in props: + self.name = props["name"] diff --git a/src/its_preselector/web_relay.py b/src/its_preselector/web_relay.py index 4f0e5a9..7dde8c2 100644 --- a/src/its_preselector/web_relay.py +++ b/src/its_preselector/web_relay.py @@ -5,17 +5,14 @@ class WebRelay(ABC): def __init__(self, config): self.config = config - @abstractmethod def get_sensor_value(sensor): pass - @abstractmethod def set_state(self, i): pass - @abstractmethod def healthy(self): pass @@ -32,4 +29,4 @@ def name(self): @abstractmethod def get_status(self): - pass \ No newline at end of file + pass diff --git a/src/its_preselector/web_relay_preselector.py b/src/its_preselector/web_relay_preselector.py index 1104586..f515fcb 100644 --- a/src/its_preselector/web_relay_preselector.py +++ b/src/its_preselector/web_relay_preselector.py @@ -1,13 +1,12 @@ import logging + from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay from its_preselector.preselector import Preselector - logger = logging.getLogger(__name__) class WebRelayPreselector(Preselector): - def __init__(self, sigmf, config): super().__init__(sigmf, config) self.web_relay = ControlByWebWebRelay(config) @@ -31,5 +30,3 @@ def name(self): def get_status(self): return self.web_relay.get_status() - - diff --git a/tests/null_preselector.sigmf-meta b/tests/null_preselector.sigmf-meta index a6c4d67..87c1712 100644 --- a/tests/null_preselector.sigmf-meta +++ b/tests/null_preselector.sigmf-meta @@ -104,4 +104,4 @@ "ntia-sensor:mean_noise_power_units": "dBm" } ] -} \ No newline at end of file +} diff --git a/tests/test_amplifier.py b/tests/test_amplifier.py index cc474fe..bd9bba8 100644 --- a/tests/test_amplifier.py +++ b/tests/test_amplifier.py @@ -1,19 +1,19 @@ -import unittest -from its_preselector.web_relay_preselector import WebRelayPreselector import json +import unittest from pathlib import Path +from its_preselector.web_relay_preselector import WebRelayPreselector -class TestAmplifier(unittest.TestCase): +class TestAmplifier(unittest.TestCase): @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / 'test_metadata.sigmf-meta') + file = open(fpath / "test_metadata.sigmf-meta") sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / 'null_preselector.sigmf-meta') + null_file = open(fpath / "null_preselector.sigmf-meta") null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -30,12 +30,15 @@ def test_valid_amplifier_spec(self): spec = self.preselector.amplifiers[0].amplifier_spec self.assertEqual("1502150", spec.id) self.assertEqual("MITEQ AFS44-00101800-25-10P-44", spec.model) - self.assertEqual("https://nardamiteq.com/docs/MITEQ_Amplifier-AFS.JS_c41.pdf", spec.supplemental_information) + self.assertEqual( + "https://nardamiteq.com/docs/MITEQ_Amplifier-AFS.JS_c41.pdf", + spec.supplemental_information, + ) def test_empty_amplifiers(self): self.assertIsNotNone(self.empty_preselector.amplifiers) self.assertEqual(0, len(self.empty_preselector.amplifiers)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_cal_source.py b/tests/test_cal_source.py index 79dd3f3..aa9f22e 100644 --- a/tests/test_cal_source.py +++ b/tests/test_cal_source.py @@ -1,18 +1,19 @@ -import unittest -from its_preselector.web_relay_preselector import WebRelayPreselector import json +import unittest from pathlib import Path -class TestCalSource(unittest.TestCase): +from its_preselector.web_relay_preselector import WebRelayPreselector + +class TestCalSource(unittest.TestCase): @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / 'test_metadata.sigmf-meta') + file = open(fpath / "test_metadata.sigmf-meta") sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / 'null_preselector.sigmf-meta') + null_file = open(fpath / "null_preselector.sigmf-meta") null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -28,11 +29,15 @@ def test_valid_cal_source_spec(self): spec = self.preselector.cal_sources[0].cal_source_spec self.assertEqual("SG53400067", spec.id) self.assertEqual("Keysight 346B", spec.model) - self.assertEqual("https://www.keysight.com/en/pd-1000001299%3Aepsg%3Apro-pn-346B/noise-source-10-mhz-to-18-ghz-nominal-enr-15-db?cc=US&lc=eng",spec.supplemental_information) + self.assertEqual( + "https://www.keysight.com/en/pd-1000001299%3Aepsg%3Apro-pn-346B/noise-source-10-mhz-to-18-ghz-nominal-enr-15-db?cc=US&lc=eng", + spec.supplemental_information, + ) def test_empty_cal_source(self): self.assertIsNotNone(self.empty_preselector.cal_sources) self.assertEqual(0, len(self.empty_preselector.cal_sources)) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_controlbyweb_web_relay.py b/tests/test_controlbyweb_web_relay.py index 3bef9e2..0d919ee 100644 --- a/tests/test_controlbyweb_web_relay.py +++ b/tests/test_controlbyweb_web_relay.py @@ -1,38 +1,40 @@ import unittest -from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay -from requests import codes -from requests import Response import xml.etree.ElementTree as ET -from unittest.mock import MagicMock -from unittest.mock import PropertyMock +from unittest.mock import MagicMock, PropertyMock +from requests import Response, codes -class ControlByWebWebRelayTests(unittest.TestCase): +from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay + +class ControlByWebWebRelayTests(unittest.TestCase): @classmethod def setUpClass(cls): - cls.state = '' \ - '0' \ - '0' \ - '0' \ - '0' \ - '1' \ - '1' \ - '0' \ - '0' \ - '27.6' \ - '0' \ - '102.3' \ - '9160590' \ - '-25200' \ - '00:0C:C8:05:AA:89' \ - '''' + cls.state = ( + "" + "0" + "0" + "0" + "0" + "1" + "1" + "0" + "0" + "27.6" + "0" + "102.3" + "9160590" + "-25200" + "00:0C:C8:05:AA:89" + "" + "" + ) def test_is_enabled(self): web_relay = ControlByWebWebRelay({}) - relay1_enabled = web_relay.is_enabled(self.state, 'relay1') + relay1_enabled = web_relay.is_enabled(self.state, "relay1") self.assertTrue(relay1_enabled) - relay3_enabled = web_relay.is_enabled(self.state, 'relay3') + relay3_enabled = web_relay.is_enabled(self.state, "relay3") self.assertFalse(relay3_enabled) # def test_get_relay_summary(self): @@ -42,52 +44,70 @@ def test_is_enabled(self): def test_state_matches(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}}) - self.assertTrue(web_relay.state_matches('relay1=1', root)) + web_relay = ControlByWebWebRelay( + { + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + } + } + ) + self.assertTrue(web_relay.state_matches("relay1=1", root)) def test_get_state_from_config(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, - 'status_states': { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": 'relay2=1,relay1=1', - "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' - }}) + web_relay = ControlByWebWebRelay( + { + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + }, + "status_states": { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": "relay2=1,relay1=1", + "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", + }, + } + ) response = Response() response.status_code = codes.ok - type(response).text = PropertyMock(return_value = self.state) + type(response).text = PropertyMock(return_value=self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() self.assertEqual(len(states.keys()), 6) - self.assertTrue(states['noise diode powered']) - self.assertFalse(states['antenna path enabled']) - self.assertFalse(states['measurements']) - self.assertTrue(states['noise diode path enabled']) - self.assertTrue(states['noise on']) + self.assertTrue(states["noise diode powered"]) + self.assertFalse(states["antenna path enabled"]) + self.assertFalse(states["measurements"]) + self.assertTrue(states["noise diode path enabled"]) + self.assertTrue(states["noise on"]) def test_get_status(self): - web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, - 'status_states': { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": 'relay2=1,relay1=1', - "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' - }}) + web_relay = ControlByWebWebRelay( + { + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + }, + "status_states": { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": "relay2=1,relay1=1", + "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", + }, + } + ) response = Response() response.status_code = codes.ok type(response).text = PropertyMock(return_value=self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() self.assertEqual(len(states.keys()), 6) - self.assertTrue(states['noise diode powered']) - self.assertFalse(states['antenna path enabled']) - self.assertFalse(states['measurements']) - self.assertTrue(states['noise diode path enabled']) - self.assertTrue(states['noise on']) + self.assertTrue(states["noise diode powered"]) + self.assertFalse(states["antenna path enabled"]) + self.assertFalse(states["measurements"]) + self.assertTrue(states["noise diode path enabled"]) + self.assertTrue(states["noise on"]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_filter.py b/tests/test_filter.py index 93b1219..ce52fca 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -1,18 +1,19 @@ -import unittest -from its_preselector.web_relay_preselector import WebRelayPreselector import json +import unittest from pathlib import Path -class TestFilter(unittest.TestCase): +from its_preselector.web_relay_preselector import WebRelayPreselector + +class TestFilter(unittest.TestCase): @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / 'test_metadata.sigmf-meta') + file = open(fpath / "test_metadata.sigmf-meta") sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / 'null_preselector.sigmf-meta') + null_file = open(fpath / "null_preselector.sigmf-meta") null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -21,12 +22,15 @@ def test_valid_filter_spec(self): spec = self.preselector.filters[0].filter_spec self.assertEqual("13FV40, SN 9", spec.id) self.assertEqual("K&L 13FV40-3625/U150-o/o", spec.model) - self.assertEqual("http://www.klfilterwizard.com/klfwpart.aspx?FWS=1112001&PN=13FV40-3625%2fU150-O%2fO",spec.supplemental_information) + self.assertEqual( + "http://www.klfilterwizard.com/klfwpart.aspx?FWS=1112001&PN=13FV40-3625%2fU150-O%2fO", + spec.supplemental_information, + ) def test_valid_filter(self): self.assertEqual(1, len(self.preselector.amplifiers)) amplifier = self.preselector.filters[0] - self.assertEqual(3550000000.0,amplifier.frequency_low_stopband) + self.assertEqual(3550000000.0, amplifier.frequency_low_stopband) self.assertEqual(3700000000.0, amplifier.frequency_high_stopband) self.assertEqual(3000000000.0, amplifier.frequency_low_passband) self.assertEqual(3750000000.0, amplifier.frequency_high_passband) @@ -36,5 +40,5 @@ def test_empty_filter(self): self.assertEqual(0, len(self.empty_preselector.filters)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_metadata.sigmf-meta b/tests/test_metadata.sigmf-meta index 5eb39fd..1dbe0ff 100644 --- a/tests/test_metadata.sigmf-meta +++ b/tests/test_metadata.sigmf-meta @@ -144,4 +144,4 @@ "ntia-sensor:mean_noise_power_units": "dBm" } ] -} \ No newline at end of file +} diff --git a/tests/test_preselector.py b/tests/test_preselector.py index 61c0e7d..53b49b2 100644 --- a/tests/test_preselector.py +++ b/tests/test_preselector.py @@ -1,23 +1,23 @@ -import unittest -from its_preselector.web_relay_preselector import WebRelayPreselector import json +import unittest from pathlib import Path +from its_preselector.web_relay_preselector import WebRelayPreselector -class TestWebRelayPreselector(unittest.TestCase): +class TestWebRelayPreselector(unittest.TestCase): @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / 'test_metadata.sigmf-meta') + file = open(fpath / "test_metadata.sigmf-meta") sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / 'null_preselector.sigmf-meta') + null_file = open(fpath / "null_preselector.sigmf-meta") null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) - with open(fpath / 'sensor_definition.json', 'r') as f: + with open(fpath / "sensor_definition.json", "r") as f: sensor_def = json.load(f) cls.scos_preselector = WebRelayPreselector(sensor_def, {}) @@ -29,30 +29,40 @@ def test_empty_preselector(self): def test_empty_valid_frequency_low_passband(self): self.assertEqual(3000000000.0, self.preselector.get_frequency_low_passband(0)) - self.assertEqual(self.preselector.get_frequency_low_passband(0), self.preselector.get_frequency_low_passband(1)) + self.assertEqual( + self.preselector.get_frequency_low_passband(0), + self.preselector.get_frequency_low_passband(1), + ) def test_empty_get_frequency_low_passband(self): self.assertIsNone(self.empty_preselector.get_frequency_low_passband(0)) def test_valid_get_frequency_high_passband(self): self.assertEqual(3750000000.0, self.preselector.get_frequency_high_passband(0)) - self.assertEqual(self.preselector.get_frequency_high_passband(0), - self.preselector.get_frequency_high_passband(1)) + self.assertEqual( + self.preselector.get_frequency_high_passband(0), + self.preselector.get_frequency_high_passband(1), + ) def test_empty_get_frequency_high_passband(self): self.assertIsNone(self.empty_preselector.get_frequency_high_passband(0)) def test_valid_get_frequency_low_stopband(self): self.assertEqual(3550000000.0, self.preselector.get_frequency_low_stopband(0)) - self.assertEqual(self.preselector.get_frequency_low_stopband(0), self.preselector.get_frequency_low_stopband(1)) + self.assertEqual( + self.preselector.get_frequency_low_stopband(0), + self.preselector.get_frequency_low_stopband(1), + ) def test_empty_get_frequency_low_stopband(self): self.assertIsNone(self.empty_preselector.get_frequency_low_stopband(0)) def test_valid_get_frequency_high_stopband(self): self.assertEqual(3700000000.0, self.preselector.get_frequency_high_stopband(0)) - self.assertEqual(self.preselector.get_frequency_high_stopband(0), - self.preselector.get_frequency_high_stopband(1)) + self.assertEqual( + self.preselector.get_frequency_high_stopband(0), + self.preselector.get_frequency_high_stopband(1), + ) def test_empty_get_frequency_high_stopband(self): self.assertIsNone(self.empty_preselector.get_frequency_high_stopband(0)) @@ -73,5 +83,5 @@ def test_scos_calibration_sources(self): self.assertEqual(1, len(self.scos_preselector.cal_sources)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_rfpaths.py b/tests/test_rfpaths.py index 9a70745..325ccd9 100644 --- a/tests/test_rfpaths.py +++ b/tests/test_rfpaths.py @@ -1,18 +1,19 @@ import json import unittest -from its_preselector.web_relay_preselector import WebRelayPreselector from pathlib import Path -class TestRFPaths(unittest.TestCase): +from its_preselector.web_relay_preselector import WebRelayPreselector + +class TestRFPaths(unittest.TestCase): @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / 'test_metadata.sigmf-meta') + file = open(fpath / "test_metadata.sigmf-meta") sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / 'null_preselector.sigmf-meta') + null_file = open(fpath / "null_preselector.sigmf-meta") null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -25,17 +26,17 @@ def test_empty_paths(self): self.assertEqual(0, len(self.empty_preselector.rf_paths)) def test_name(self): - self.assertEqual('noise_diode_on', self.preselector.rf_paths[0].name) + self.assertEqual("noise_diode_on", self.preselector.rf_paths[0].name) def test_cal_source_id(self): - self.assertEqual('SG53400067', self.preselector.rf_paths[0].cal_source_id) + self.assertEqual("SG53400067", self.preselector.rf_paths[0].cal_source_id) def test_filter_id(self): - self.assertEqual('13FV40, SN 9', self.preselector.rf_paths[0].filter_id) + self.assertEqual("13FV40, SN 9", self.preselector.rf_paths[0].filter_id) def test_amplifier_id(self): - self.assertEqual('1502150', self.preselector.rf_paths[0].amplifier_id) + self.assertEqual("1502150", self.preselector.rf_paths[0].amplifier_id) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_web_relay_preselector.py b/tests/test_web_relay_preselector.py index ca94261..e5b8da5 100644 --- a/tests/test_web_relay_preselector.py +++ b/tests/test_web_relay_preselector.py @@ -1,66 +1,87 @@ -import unittest import json +import unittest from pathlib import Path + from its_preselector.web_relay_preselector import WebRelayPreselector class MyTestCase(unittest.TestCase): - @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / 'test_metadata.sigmf-meta') + file = open(fpath / "test_metadata.sigmf-meta") cls.sensor_def = json.load(file) file.close() def test_blank_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, - {'base_url': '', 'antenna': '1State=0,2State=0,3State=0,4State=0'}) + preselector = WebRelayPreselector( + self.sensor_def, + {"base_url": "", "antenna": "1State=0,2State=0,3State=0,4State=0"}, + ) with self.assertRaises(Exception): - preselector.set_state('antenna') + preselector.set_state("antenna") def test_none_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, - {'base_url': None, 'antenna': '1State=0,2State=0,3State=0,4State=0'}) + preselector = WebRelayPreselector( + self.sensor_def, + {"base_url": None, "antenna": "1State=0,2State=0,3State=0,4State=0"}, + ) with self.assertRaises(Exception): - preselector.set_state('antenna') + preselector.set_state("antenna") def test_invalid_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, {'base_url': 'http://badpreselector.gov', - 'antenna': '1State=0,2State=0,3State=0,4State=0'}) + preselector = WebRelayPreselector( + self.sensor_def, + { + "base_url": "http://badpreselector.gov", + "antenna": "1State=0,2State=0,3State=0,4State=0", + }, + ) with self.assertRaises(Exception): - preselector.set_state('antenna') + preselector.set_state("antenna") def test_healthy_false(self): - preselector =WebRelayPreselector(self.sensor_def, { - 'name': 'preselector', - 'base_url': 'http://bad_preselector.gov', - 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, - 'status_states': { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": 'relay2=1,relay1=1', - "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' - }}) + preselector = WebRelayPreselector( + self.sensor_def, + { + "name": "preselector", + "base_url": "http://bad_preselector.gov", + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + }, + "status_states": { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": "relay2=1,relay1=1", + "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", + }, + }, + ) self.assertFalse(preselector.healthy()) def test_get_status_bad_url(self): - preselector = WebRelayPreselector(self.sensor_def, { - 'name': 'preselector', - 'base_url': 'http://bad_preselector.gov', - 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, - 'status_states': { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": 'relay2=1,relay1=1', - "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' - }}) + preselector = WebRelayPreselector( + self.sensor_def, + { + "name": "preselector", + "base_url": "http://bad_preselector.gov", + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + }, + "status_states": { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": "relay2=1,relay1=1", + "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", + }, + }, + ) status = preselector.get_status() - self.assertFalse(status['web_relay_healthy']) + self.assertFalse(status["web_relay_healthy"]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From f9193cbbc4f4971d73ee8c217b20e893375a838e Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Fri, 12 Aug 2022 09:20:01 -0600 Subject: [PATCH 13/30] Set timeout to 1 second when querying for switch state. --- src/its_preselector/controlbyweb_web_relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 12d22a5..629d563 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -129,7 +129,7 @@ def get_relay_state(state_xml, relay): def get_state_xml(self): if self.base_url and self.base_url != '': - response = requests.get(self.base_url) + response = requests.get(self.base_url, timeout=1) return response else: raise Exception('base_url is None or blank') From efda2a91715ab54515fdce292e576dbd4a9da659 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Fri, 12 Aug 2022 09:22:24 -0600 Subject: [PATCH 14/30] Revert "pre-commit run on all files" This reverts commit b2e89ed7e192dcf2f980b8f92d08bcad960818ac. --- .gitignore | 2 +- LICENSE.md | 5 +- config/config.json | 2 +- config/metadata.sigmf-meta | 2 +- src/its_preselector/amplifier.py | 17 +-- src/its_preselector/cal_source.py | 13 +- src/its_preselector/controlbyweb_web_relay.py | 67 ++++----- src/its_preselector/filter.py | 21 +-- src/its_preselector/hardware_spec.py | 19 +-- src/its_preselector/preselector.py | 50 +++---- src/its_preselector/rf_path.py | 18 +-- src/its_preselector/web_relay.py | 5 +- src/its_preselector/web_relay_preselector.py | 5 +- tests/null_preselector.sigmf-meta | 2 +- tests/test_amplifier.py | 17 +-- tests/test_cal_source.py | 19 +-- tests/test_controlbyweb_web_relay.py | 128 ++++++++---------- tests/test_filter.py | 20 ++- tests/test_metadata.sigmf-meta | 2 +- tests/test_preselector.py | 36 ++--- tests/test_rfpaths.py | 19 ++- tests/test_web_relay_preselector.py | 93 +++++-------- 22 files changed, 246 insertions(+), 316 deletions(-) diff --git a/.gitignore b/.gitignore index 13d04e1..b234207 100644 --- a/.gitignore +++ b/.gitignore @@ -147,4 +147,4 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties -fabric.properties +fabric.properties \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index fd7fe11..12746a1 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,12 +2,13 @@ SOFTWARE DISCLAIMER / RELEASE This software was developed by employees of the National Telecommunications and Information Administration (NTIA), an agency of the Federal Government and is provided to you as a public service. Pursuant to Title 15 United States Code Section 105, works of NTIA employees are not subject to copyright protection within the United States. -The software is provided by NTIA “AS IS.” NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does not warrant or make any representations regarding the use of the software or the results thereof, including but not limited to the correctness, accuracy, reliability or usefulness of the software. +The software is provided by NTIA “AS IS.” NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does not warrant or make any representations regarding the use of the software or the results thereof, including but not limited to the correctness, accuracy, reliability or usefulness of the software. To the extent that NTIA holds rights in countries other than the United States, you are hereby granted the non-exclusive irrevocable and unconditional right to print, publish, prepare derivative works and distribute the NTIA software, in any medium, or authorize others to do so on your behalf, on a royalty-free basis throughout the World. You may improve, modify, and create derivative works of the software or any portion of the software, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the software and should note the date and nature of any such change. -You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. +You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. Please provide appropriate acknowledgments of NTIA’s creation of the software in any copies or derivative works of this software. + diff --git a/config/config.json b/config/config.json index 405c38b..83195b5 100644 --- a/config/config.json +++ b/config/config.json @@ -11,4 +11,4 @@ "noise diode path enabled": "relay1=1", } -} +} \ No newline at end of file diff --git a/config/metadata.sigmf-meta b/config/metadata.sigmf-meta index 7c2c2dd..42c7c29 100644 --- a/config/metadata.sigmf-meta +++ b/config/metadata.sigmf-meta @@ -164,4 +164,4 @@ "ntia-core:azimuth_angle" : 90.0, "ntia-core:elevation_angle" : 0.0 } ] -} +} \ No newline at end of file diff --git a/src/its_preselector/amplifier.py b/src/its_preselector/amplifier.py index b157ca5..d3a94ea 100644 --- a/src/its_preselector/amplifier.py +++ b/src/its_preselector/amplifier.py @@ -2,16 +2,17 @@ class Amplifier: + def __init__(self, meta): self.amplifier_spec = None self.gain = None self.noise_figure = None self.max_power = None - if "amplifier_spec" in meta: - self.amplifier_spec = HardwareSpec(meta["amplifier_spec"]) - if "gain" in meta: - self.gain = meta["gain"] - if "noise_figure" in meta: - self.noise_figure = meta["noise_figure"] - if "max_power" in meta: - self.max_power = meta["max_power"] + if 'amplifier_spec' in meta: + self.amplifier_spec = HardwareSpec(meta['amplifier_spec']) + if 'gain' in meta: + self.gain = meta['gain'] + if 'noise_figure' in meta: + self.noise_figure = meta['noise_figure'] + if 'max_power' in meta: + self.max_power = meta['max_power'] diff --git a/src/its_preselector/cal_source.py b/src/its_preselector/cal_source.py index 35e8cc3..028ce4d 100644 --- a/src/its_preselector/cal_source.py +++ b/src/its_preselector/cal_source.py @@ -2,13 +2,14 @@ class CalSource: + def __init__(self, props): self.cal_source_spec = None self.type = None self.enr = None - if "cal_source_spec" in props: - self.cal_source_spec = HardwareSpec(props["cal_source_spec"]) - if "type" in props: - self.type = props["type"] - if "enr" in props: - self.enr = props["enr"] + if 'cal_source_spec' in props: + self.cal_source_spec = HardwareSpec(props['cal_source_spec']) + if 'type' in props: + self.type = props['type'] + if 'enr' in props: + self.enr = props['enr'] diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index b8254a7..12d22a5 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -1,29 +1,28 @@ +from its_preselector.web_relay import WebRelay import logging -import xml.etree.ElementTree as ET - import requests - -from its_preselector.web_relay import WebRelay +import xml.etree.ElementTree as ET logger = logging.getLogger(__name__) class ControlByWebWebRelay(WebRelay): + def __init__(self, config): super().__init__(config) - if "base_url" in config: - self.base_url = config["base_url"] + if 'base_url' in config: + self.base_url = config['base_url'] def get_sensor_value(self, sensor_num): sensor_num_string = str(sensor_num) response = requests.get(self.base_url) # Check for X310 xml format first. - sensor_tag = "sensor" + sensor_num_string + sensor_tag = 'sensor' + sensor_num_string root = ET.fromstring(response.text) sensor = root.find(sensor_tag) if sensor is None: # Didn't find X310 format sensor so check for X410 format. - sensor_tag = "oneWireSensor" + sensor_num_string + sensor_tag = 'oneWireSensor' + sensor_num_string sensor = root.find(sensor_tag) if sensor is None: return None @@ -31,19 +30,17 @@ def get_sensor_value(self, sensor_num): return sensor.text def set_state(self, key): - if key in self.config["control_states"]: - switches = self.config["control_states"][str(key)].split(",") - if self.base_url and self.base_url != "": + if key in self.config['control_states']: + switches = self.config['control_states'][str(key)].split(',') + if self.base_url and self.base_url != '': for i in range(len(switches)): - command = self.base_url + "?relay" + switches[i] + command = self.base_url + '?relay' + switches[i] logger.debug(command) response = requests.get(command) if response.status_code != requests.codes.ok: - raise Exception( - "Unable to set preselector state. Verify configuration and connectivity." - ) + raise Exception('Unable to set preselector state. Verify configuration and connectivity.') else: - raise Exception("base_url is None or blank") + raise Exception('base_url is None or blank') else: raise Exception("RF path " + key + " configuration does not exist.") @@ -61,7 +58,7 @@ def id(self): @property def name(self): - return self.config["name"] + return self.config['name'] def get_status(self): state = {} @@ -73,38 +70,32 @@ def get_status(self): state_xml = response.text xml_root = ET.fromstring(state_xml) - for key, value in self.config["status_states"].items(): - relay_states = value.split(",") + for key, value in self.config['status_states'].items(): + relay_states = value.split(',') matches = True for relay_state in relay_states: matches = matches and self.state_matches(relay_state, xml_root) state[key] = matches except: - logger.error("Unable to get status") - state["web_relay_healthy"] = healthy + logger.error('Unable to get status') + state['web_relay_healthy'] = healthy return state def state_matches(self, relay_and_state, xml_root): - relay_state_list = relay_and_state.split("=") + relay_state_list = relay_and_state.split('=') desired_state = relay_state_list[1] relay_tag = relay_state_list[0] relay_element = xml_root.find(relay_tag) if relay_element is None: - raise Exception("Unable to locate " + relay_tag) + raise Exception('Unable to locate ' + relay_tag) else: return desired_state == relay_element.text def get_state_summary(self, response): - relay_state = ( - "1State=" - + self.get_relay_state(response, "relay1") - + ",2State=" - + self.get_relay_state(response, "relay2") - + ",3State=" - + self.get_relay_state(response, "relay3") - + ",4State=" - + self.get_relay_state(response, "relay4") - ) + relay_state = '1State=' + self.get_relay_state(response, 'relay1') + \ + ',2State=' + self.get_relay_state(response, 'relay2') + \ + ',3State=' + self.get_relay_state(response, 'relay3') + \ + ',4State=' + self.get_relay_state(response, 'relay4') return relay_state def map_relay_state_to_config(self, relay_state): @@ -118,10 +109,10 @@ def is_enabled(state_xml, relay): root = ET.fromstring(state_xml) relay_node = root.find(relay) if relay_node is None: - raise Exception("Relay " + relay + " does not exist.") + raise Exception('Relay ' + relay + ' does not exist.') else: relay_state = relay_node.text - if relay_state == "1": + if relay_state == '1': return True else: return False @@ -131,14 +122,14 @@ def get_relay_state(state_xml, relay): root = ET.fromstring(state_xml) relay_node = root.find(relay) if relay_node is None: - raise Exception("Relay " + relay + " does not exist.") + raise Exception('Relay ' + relay + ' does not exist.') else: relay_state = relay_node.text return relay_state def get_state_xml(self): - if self.base_url and self.base_url != "": + if self.base_url and self.base_url != '': response = requests.get(self.base_url) return response else: - raise Exception("base_url is None or blank") + raise Exception('base_url is None or blank') diff --git a/src/its_preselector/filter.py b/src/its_preselector/filter.py index 24b566e..966c093 100644 --- a/src/its_preselector/filter.py +++ b/src/its_preselector/filter.py @@ -2,19 +2,20 @@ class Filter: + def __init__(self, props): self.filter_spec = None self.frequency_low_passband = None self.frequency_high_passband = None self.frequency_low_stopband = None self.frequency_high_stopband = None - if "filter_spec" in props: - self.filter_spec = HardwareSpec(props["filter_spec"]) - if "frequency_low_passband" in props: - self.frequency_low_passband = props["frequency_low_passband"] - if "frequency_high_passband" in props: - self.frequency_high_passband = props["frequency_high_passband"] - if "frequency_low_stopband" in props: - self.frequency_low_stopband = props["frequency_low_stopband"] - if "frequency_high_stopband" in props: - self.frequency_high_stopband = props["frequency_high_stopband"] + if 'filter_spec' in props: + self.filter_spec = HardwareSpec(props['filter_spec']) + if 'frequency_low_passband' in props: + self.frequency_low_passband = props['frequency_low_passband'] + if 'frequency_high_passband' in props: + self.frequency_high_passband = props['frequency_high_passband'] + if 'frequency_low_stopband' in props: + self.frequency_low_stopband = props['frequency_low_stopband'] + if 'frequency_high_stopband' in props: + self.frequency_high_stopband = props['frequency_high_stopband'] diff --git a/src/its_preselector/hardware_spec.py b/src/its_preselector/hardware_spec.py index 1237998..caf35d9 100644 --- a/src/its_preselector/hardware_spec.py +++ b/src/its_preselector/hardware_spec.py @@ -1,14 +1,15 @@ class HardwareSpec: + def __init__(self, props): - self.id = None + self.id=None self.model = None self.version = None self.supplemental_information = None - if "id" in props: - self.id = props["id"] - if "model" in props: - self.model = props["model"] - if "supplemental_information" in props: - self.supplemental_information = props["supplemental_information"] - if "version" in props: - self.version = props["version"] + if 'id' in props: + self.id = props['id'] + if 'model' in props: + self.model = props['model'] + if 'supplemental_information' in props: + self.supplemental_information = props['supplemental_information'] + if 'version' in props: + self.version = props['version'] diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index 9d089b4..5ce5ce5 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -1,13 +1,13 @@ from abc import ABC, abstractmethod, abstractproperty - +from its_preselector.rf_path import RfPath +from its_preselector.filter import Filter from its_preselector.amplifier import Amplifier from its_preselector.cal_source import CalSource -from its_preselector.filter import Filter from its_preselector.hardware_spec import HardwareSpec -from its_preselector.rf_path import RfPath class Preselector(ABC): + def __init__(self, sigmf, config): self.amplifiers = [] self.rf_paths = [] @@ -16,56 +16,44 @@ def __init__(self, sigmf, config): self.preselector_spec = [] self.config = config try: - if "global" in sigmf: - self.__set_filters( - sigmf["global"]["ntia-sensor:sensor"]["preselector"]["filters"] - ) + if 'global' in sigmf: + self.__set_filters(sigmf['global']['ntia-sensor:sensor']['preselector']['filters']) else: - self.__set_filters(sigmf["preselector"]["filters"]) + self.__set_filters(sigmf['preselector']['filters']) except KeyError: pass try: - if "global" in sigmf: - self.__set_amplifiers( - sigmf["global"]["ntia-sensor:sensor"]["preselector"]["amplifiers"] - ) + if 'global' in sigmf: + self.__set_amplifiers(sigmf['global']['ntia-sensor:sensor']['preselector']['amplifiers']) else: - self.__set_amplifiers(sigmf["preselector"]["amplifiers"]) + self.__set_amplifiers(sigmf['preselector']['amplifiers']) except KeyError: pass try: - if "global" in sigmf: - self.__get_rf_paths( - sigmf["global"]["ntia-sensor:sensor"]["preselector"]["rf_paths"] - ) + if 'global' in sigmf: + self.__get_rf_paths(sigmf['global']['ntia-sensor:sensor']['preselector']['rf_paths']) else: - self.__get_rf_paths(sigmf["preselector"]["rf_paths"]) + self.__get_rf_paths(sigmf['preselector']['rf_paths']) except KeyError: pass try: - if "global" in sigmf: - self.__set_cal_sources( - sigmf["global"]["ntia-sensor:sensor"]["preselector"]["cal_sources"] - ) + if 'global' in sigmf: + self.__set_cal_sources(sigmf['global']['ntia-sensor:sensor']['preselector']['cal_sources']) else: - self.__set_cal_sources(sigmf["preselector"]["cal_sources"]) + self.__set_cal_sources(sigmf['preselector']['cal_sources']) except KeyError: pass try: - if "global" in sigmf: + if 'global' in sigmf: self.preselector_spec = HardwareSpec( - sigmf["global"]["ntia-sensor:sensor"]["preselector"][ - "preselector_spec" - ] - ) + sigmf['global']['ntia-sensor:sensor']['preselector']['preselector_spec']) else: self.preselector_spec = HardwareSpec( - sigmf["preselector"]["preselector_spec"] - ) + sigmf['preselector']['preselector_spec']) except KeyError: pass @@ -167,6 +155,7 @@ def __get_amplifier(self, amp_id): def get_sensor_value(self, sensor): pass + @property @abstractmethod def id(self): @@ -180,3 +169,4 @@ def name(self) -> str: @abstractmethod def get_status(self) -> dict: pass + diff --git a/src/its_preselector/rf_path.py b/src/its_preselector/rf_path.py index bec38a4..2bad120 100644 --- a/src/its_preselector/rf_path.py +++ b/src/its_preselector/rf_path.py @@ -1,11 +1,13 @@ class RfPath: + def __init__(self, props): self.name = None - if "cal_source_id" in props: - self.cal_source_id = props["cal_source_id"] - if "filter_id" in props: - self.filter_id = props["filter_id"] - if "amplifier_id" in props: - self.amplifier_id = props["amplifier_id"] - if "name" in props: - self.name = props["name"] + if 'cal_source_id' in props: + self.cal_source_id = props['cal_source_id'] + if 'filter_id' in props: + self.filter_id = props['filter_id'] + if 'amplifier_id' in props: + self.amplifier_id = props['amplifier_id'] + if 'name' in props: + self.name = props['name'] + diff --git a/src/its_preselector/web_relay.py b/src/its_preselector/web_relay.py index 7dde8c2..4f0e5a9 100644 --- a/src/its_preselector/web_relay.py +++ b/src/its_preselector/web_relay.py @@ -5,14 +5,17 @@ class WebRelay(ABC): def __init__(self, config): self.config = config + @abstractmethod def get_sensor_value(sensor): pass + @abstractmethod def set_state(self, i): pass + @abstractmethod def healthy(self): pass @@ -29,4 +32,4 @@ def name(self): @abstractmethod def get_status(self): - pass + pass \ No newline at end of file diff --git a/src/its_preselector/web_relay_preselector.py b/src/its_preselector/web_relay_preselector.py index f515fcb..1104586 100644 --- a/src/its_preselector/web_relay_preselector.py +++ b/src/its_preselector/web_relay_preselector.py @@ -1,12 +1,13 @@ import logging - from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay from its_preselector.preselector import Preselector + logger = logging.getLogger(__name__) class WebRelayPreselector(Preselector): + def __init__(self, sigmf, config): super().__init__(sigmf, config) self.web_relay = ControlByWebWebRelay(config) @@ -30,3 +31,5 @@ def name(self): def get_status(self): return self.web_relay.get_status() + + diff --git a/tests/null_preselector.sigmf-meta b/tests/null_preselector.sigmf-meta index 87c1712..a6c4d67 100644 --- a/tests/null_preselector.sigmf-meta +++ b/tests/null_preselector.sigmf-meta @@ -104,4 +104,4 @@ "ntia-sensor:mean_noise_power_units": "dBm" } ] -} +} \ No newline at end of file diff --git a/tests/test_amplifier.py b/tests/test_amplifier.py index bd9bba8..cc474fe 100644 --- a/tests/test_amplifier.py +++ b/tests/test_amplifier.py @@ -1,19 +1,19 @@ -import json import unittest -from pathlib import Path - from its_preselector.web_relay_preselector import WebRelayPreselector +import json +from pathlib import Path class TestAmplifier(unittest.TestCase): + @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / "test_metadata.sigmf-meta") + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / "null_preselector.sigmf-meta") + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -30,15 +30,12 @@ def test_valid_amplifier_spec(self): spec = self.preselector.amplifiers[0].amplifier_spec self.assertEqual("1502150", spec.id) self.assertEqual("MITEQ AFS44-00101800-25-10P-44", spec.model) - self.assertEqual( - "https://nardamiteq.com/docs/MITEQ_Amplifier-AFS.JS_c41.pdf", - spec.supplemental_information, - ) + self.assertEqual("https://nardamiteq.com/docs/MITEQ_Amplifier-AFS.JS_c41.pdf", spec.supplemental_information) def test_empty_amplifiers(self): self.assertIsNotNone(self.empty_preselector.amplifiers) self.assertEqual(0, len(self.empty_preselector.amplifiers)) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/tests/test_cal_source.py b/tests/test_cal_source.py index aa9f22e..79dd3f3 100644 --- a/tests/test_cal_source.py +++ b/tests/test_cal_source.py @@ -1,19 +1,18 @@ -import json import unittest -from pathlib import Path - from its_preselector.web_relay_preselector import WebRelayPreselector - +import json +from pathlib import Path class TestCalSource(unittest.TestCase): + @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / "test_metadata.sigmf-meta") + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / "null_preselector.sigmf-meta") + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -29,15 +28,11 @@ def test_valid_cal_source_spec(self): spec = self.preselector.cal_sources[0].cal_source_spec self.assertEqual("SG53400067", spec.id) self.assertEqual("Keysight 346B", spec.model) - self.assertEqual( - "https://www.keysight.com/en/pd-1000001299%3Aepsg%3Apro-pn-346B/noise-source-10-mhz-to-18-ghz-nominal-enr-15-db?cc=US&lc=eng", - spec.supplemental_information, - ) + self.assertEqual("https://www.keysight.com/en/pd-1000001299%3Aepsg%3Apro-pn-346B/noise-source-10-mhz-to-18-ghz-nominal-enr-15-db?cc=US&lc=eng",spec.supplemental_information) def test_empty_cal_source(self): self.assertIsNotNone(self.empty_preselector.cal_sources) self.assertEqual(0, len(self.empty_preselector.cal_sources)) - -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/tests/test_controlbyweb_web_relay.py b/tests/test_controlbyweb_web_relay.py index 0d919ee..3bef9e2 100644 --- a/tests/test_controlbyweb_web_relay.py +++ b/tests/test_controlbyweb_web_relay.py @@ -1,40 +1,38 @@ import unittest -import xml.etree.ElementTree as ET -from unittest.mock import MagicMock, PropertyMock - -from requests import Response, codes - from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay +from requests import codes +from requests import Response +import xml.etree.ElementTree as ET +from unittest.mock import MagicMock +from unittest.mock import PropertyMock class ControlByWebWebRelayTests(unittest.TestCase): + @classmethod def setUpClass(cls): - cls.state = ( - "" - "0" - "0" - "0" - "0" - "1" - "1" - "0" - "0" - "27.6" - "0" - "102.3" - "9160590" - "-25200" - "00:0C:C8:05:AA:89" - "" - "" - ) + cls.state = '' \ + '0' \ + '0' \ + '0' \ + '0' \ + '1' \ + '1' \ + '0' \ + '0' \ + '27.6' \ + '0' \ + '102.3' \ + '9160590' \ + '-25200' \ + '00:0C:C8:05:AA:89' \ + '''' def test_is_enabled(self): web_relay = ControlByWebWebRelay({}) - relay1_enabled = web_relay.is_enabled(self.state, "relay1") + relay1_enabled = web_relay.is_enabled(self.state, 'relay1') self.assertTrue(relay1_enabled) - relay3_enabled = web_relay.is_enabled(self.state, "relay3") + relay3_enabled = web_relay.is_enabled(self.state, 'relay3') self.assertFalse(relay3_enabled) # def test_get_relay_summary(self): @@ -44,70 +42,52 @@ def test_is_enabled(self): def test_state_matches(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay( - { - "control_states": { - "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" - } - } - ) - self.assertTrue(web_relay.state_matches("relay1=1", root)) + web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}}) + self.assertTrue(web_relay.state_matches('relay1=1', root)) def test_get_state_from_config(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay( - { - "control_states": { - "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" - }, - "status_states": { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": "relay2=1,relay1=1", - "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", - }, - } - ) + web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) response = Response() response.status_code = codes.ok - type(response).text = PropertyMock(return_value=self.state) + type(response).text = PropertyMock(return_value = self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() self.assertEqual(len(states.keys()), 6) - self.assertTrue(states["noise diode powered"]) - self.assertFalse(states["antenna path enabled"]) - self.assertFalse(states["measurements"]) - self.assertTrue(states["noise diode path enabled"]) - self.assertTrue(states["noise on"]) + self.assertTrue(states['noise diode powered']) + self.assertFalse(states['antenna path enabled']) + self.assertFalse(states['measurements']) + self.assertTrue(states['noise diode path enabled']) + self.assertTrue(states['noise on']) def test_get_status(self): - web_relay = ControlByWebWebRelay( - { - "control_states": { - "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" - }, - "status_states": { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": "relay2=1,relay1=1", - "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", - }, - } - ) + web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) response = Response() response.status_code = codes.ok type(response).text = PropertyMock(return_value=self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() self.assertEqual(len(states.keys()), 6) - self.assertTrue(states["noise diode powered"]) - self.assertFalse(states["antenna path enabled"]) - self.assertFalse(states["measurements"]) - self.assertTrue(states["noise diode path enabled"]) - self.assertTrue(states["noise on"]) + self.assertTrue(states['noise diode powered']) + self.assertFalse(states['antenna path enabled']) + self.assertFalse(states['measurements']) + self.assertTrue(states['noise diode path enabled']) + self.assertTrue(states['noise on']) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/tests/test_filter.py b/tests/test_filter.py index ce52fca..93b1219 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -1,19 +1,18 @@ -import json import unittest -from pathlib import Path - from its_preselector.web_relay_preselector import WebRelayPreselector - +import json +from pathlib import Path class TestFilter(unittest.TestCase): + @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / "test_metadata.sigmf-meta") + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / "null_preselector.sigmf-meta") + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -22,15 +21,12 @@ def test_valid_filter_spec(self): spec = self.preselector.filters[0].filter_spec self.assertEqual("13FV40, SN 9", spec.id) self.assertEqual("K&L 13FV40-3625/U150-o/o", spec.model) - self.assertEqual( - "http://www.klfilterwizard.com/klfwpart.aspx?FWS=1112001&PN=13FV40-3625%2fU150-O%2fO", - spec.supplemental_information, - ) + self.assertEqual("http://www.klfilterwizard.com/klfwpart.aspx?FWS=1112001&PN=13FV40-3625%2fU150-O%2fO",spec.supplemental_information) def test_valid_filter(self): self.assertEqual(1, len(self.preselector.amplifiers)) amplifier = self.preselector.filters[0] - self.assertEqual(3550000000.0, amplifier.frequency_low_stopband) + self.assertEqual(3550000000.0,amplifier.frequency_low_stopband) self.assertEqual(3700000000.0, amplifier.frequency_high_stopband) self.assertEqual(3000000000.0, amplifier.frequency_low_passband) self.assertEqual(3750000000.0, amplifier.frequency_high_passband) @@ -40,5 +36,5 @@ def test_empty_filter(self): self.assertEqual(0, len(self.empty_preselector.filters)) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/tests/test_metadata.sigmf-meta b/tests/test_metadata.sigmf-meta index 1dbe0ff..5eb39fd 100644 --- a/tests/test_metadata.sigmf-meta +++ b/tests/test_metadata.sigmf-meta @@ -144,4 +144,4 @@ "ntia-sensor:mean_noise_power_units": "dBm" } ] -} +} \ No newline at end of file diff --git a/tests/test_preselector.py b/tests/test_preselector.py index 53b49b2..61c0e7d 100644 --- a/tests/test_preselector.py +++ b/tests/test_preselector.py @@ -1,23 +1,23 @@ -import json import unittest -from pathlib import Path - from its_preselector.web_relay_preselector import WebRelayPreselector +import json +from pathlib import Path class TestWebRelayPreselector(unittest.TestCase): + @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / "test_metadata.sigmf-meta") + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / "null_preselector.sigmf-meta") + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) - with open(fpath / "sensor_definition.json", "r") as f: + with open(fpath / 'sensor_definition.json', 'r') as f: sensor_def = json.load(f) cls.scos_preselector = WebRelayPreselector(sensor_def, {}) @@ -29,40 +29,30 @@ def test_empty_preselector(self): def test_empty_valid_frequency_low_passband(self): self.assertEqual(3000000000.0, self.preselector.get_frequency_low_passband(0)) - self.assertEqual( - self.preselector.get_frequency_low_passband(0), - self.preselector.get_frequency_low_passband(1), - ) + self.assertEqual(self.preselector.get_frequency_low_passband(0), self.preselector.get_frequency_low_passband(1)) def test_empty_get_frequency_low_passband(self): self.assertIsNone(self.empty_preselector.get_frequency_low_passband(0)) def test_valid_get_frequency_high_passband(self): self.assertEqual(3750000000.0, self.preselector.get_frequency_high_passband(0)) - self.assertEqual( - self.preselector.get_frequency_high_passband(0), - self.preselector.get_frequency_high_passband(1), - ) + self.assertEqual(self.preselector.get_frequency_high_passband(0), + self.preselector.get_frequency_high_passband(1)) def test_empty_get_frequency_high_passband(self): self.assertIsNone(self.empty_preselector.get_frequency_high_passband(0)) def test_valid_get_frequency_low_stopband(self): self.assertEqual(3550000000.0, self.preselector.get_frequency_low_stopband(0)) - self.assertEqual( - self.preselector.get_frequency_low_stopband(0), - self.preselector.get_frequency_low_stopband(1), - ) + self.assertEqual(self.preselector.get_frequency_low_stopband(0), self.preselector.get_frequency_low_stopband(1)) def test_empty_get_frequency_low_stopband(self): self.assertIsNone(self.empty_preselector.get_frequency_low_stopband(0)) def test_valid_get_frequency_high_stopband(self): self.assertEqual(3700000000.0, self.preselector.get_frequency_high_stopband(0)) - self.assertEqual( - self.preselector.get_frequency_high_stopband(0), - self.preselector.get_frequency_high_stopband(1), - ) + self.assertEqual(self.preselector.get_frequency_high_stopband(0), + self.preselector.get_frequency_high_stopband(1)) def test_empty_get_frequency_high_stopband(self): self.assertIsNone(self.empty_preselector.get_frequency_high_stopband(0)) @@ -83,5 +73,5 @@ def test_scos_calibration_sources(self): self.assertEqual(1, len(self.scos_preselector.cal_sources)) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/tests/test_rfpaths.py b/tests/test_rfpaths.py index 325ccd9..9a70745 100644 --- a/tests/test_rfpaths.py +++ b/tests/test_rfpaths.py @@ -1,19 +1,18 @@ import json import unittest -from pathlib import Path - from its_preselector.web_relay_preselector import WebRelayPreselector - +from pathlib import Path class TestRFPaths(unittest.TestCase): + @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / "test_metadata.sigmf-meta") + file = open(fpath / 'test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() cls.preselector = WebRelayPreselector(sensor_def, {}) - null_file = open(fpath / "null_preselector.sigmf-meta") + null_file = open(fpath / 'null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() cls.empty_preselector = WebRelayPreselector(null_def, {}) @@ -26,17 +25,17 @@ def test_empty_paths(self): self.assertEqual(0, len(self.empty_preselector.rf_paths)) def test_name(self): - self.assertEqual("noise_diode_on", self.preselector.rf_paths[0].name) + self.assertEqual('noise_diode_on', self.preselector.rf_paths[0].name) def test_cal_source_id(self): - self.assertEqual("SG53400067", self.preselector.rf_paths[0].cal_source_id) + self.assertEqual('SG53400067', self.preselector.rf_paths[0].cal_source_id) def test_filter_id(self): - self.assertEqual("13FV40, SN 9", self.preselector.rf_paths[0].filter_id) + self.assertEqual('13FV40, SN 9', self.preselector.rf_paths[0].filter_id) def test_amplifier_id(self): - self.assertEqual("1502150", self.preselector.rf_paths[0].amplifier_id) + self.assertEqual('1502150', self.preselector.rf_paths[0].amplifier_id) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/tests/test_web_relay_preselector.py b/tests/test_web_relay_preselector.py index e5b8da5..ca94261 100644 --- a/tests/test_web_relay_preselector.py +++ b/tests/test_web_relay_preselector.py @@ -1,87 +1,66 @@ -import json import unittest +import json from pathlib import Path - from its_preselector.web_relay_preselector import WebRelayPreselector class MyTestCase(unittest.TestCase): + @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / "test_metadata.sigmf-meta") + file = open(fpath / 'test_metadata.sigmf-meta') cls.sensor_def = json.load(file) file.close() def test_blank_base_url(self): - preselector = WebRelayPreselector( - self.sensor_def, - {"base_url": "", "antenna": "1State=0,2State=0,3State=0,4State=0"}, - ) + preselector = WebRelayPreselector(self.sensor_def, + {'base_url': '', 'antenna': '1State=0,2State=0,3State=0,4State=0'}) with self.assertRaises(Exception): - preselector.set_state("antenna") + preselector.set_state('antenna') def test_none_base_url(self): - preselector = WebRelayPreselector( - self.sensor_def, - {"base_url": None, "antenna": "1State=0,2State=0,3State=0,4State=0"}, - ) + preselector = WebRelayPreselector(self.sensor_def, + {'base_url': None, 'antenna': '1State=0,2State=0,3State=0,4State=0'}) with self.assertRaises(Exception): - preselector.set_state("antenna") + preselector.set_state('antenna') def test_invalid_base_url(self): - preselector = WebRelayPreselector( - self.sensor_def, - { - "base_url": "http://badpreselector.gov", - "antenna": "1State=0,2State=0,3State=0,4State=0", - }, - ) + preselector = WebRelayPreselector(self.sensor_def, {'base_url': 'http://badpreselector.gov', + 'antenna': '1State=0,2State=0,3State=0,4State=0'}) with self.assertRaises(Exception): - preselector.set_state("antenna") + preselector.set_state('antenna') def test_healthy_false(self): - preselector = WebRelayPreselector( - self.sensor_def, - { - "name": "preselector", - "base_url": "http://bad_preselector.gov", - "control_states": { - "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" - }, - "status_states": { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": "relay2=1,relay1=1", - "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", - }, - }, - ) + preselector =WebRelayPreselector(self.sensor_def, { + 'name': 'preselector', + 'base_url': 'http://bad_preselector.gov', + 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) self.assertFalse(preselector.healthy()) def test_get_status_bad_url(self): - preselector = WebRelayPreselector( - self.sensor_def, - { - "name": "preselector", - "base_url": "http://bad_preselector.gov", - "control_states": { - "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" - }, - "status_states": { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": "relay2=1,relay1=1", - "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", - }, - }, - ) + preselector = WebRelayPreselector(self.sensor_def, { + 'name': 'preselector', + 'base_url': 'http://bad_preselector.gov', + 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + 'status_states': { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": 'relay2=1,relay1=1', + "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' + }}) status = preselector.get_status() - self.assertFalse(status["web_relay_healthy"]) + self.assertFalse(status['web_relay_healthy']) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() From 0f9c8aa901a8d83f60e3534690c630a70c49de17 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Fri, 12 Aug 2022 09:47:53 -0600 Subject: [PATCH 15/30] replace web_relay_healthy with healthy. --- src/its_preselector/controlbyweb_web_relay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 629d563..735edda 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -78,7 +78,7 @@ def get_status(self): state[key] = matches except: logger.error('Unable to get status') - state['web_relay_healthy'] = healthy + state['healthy'] = healthy return state def state_matches(self, relay_and_state, xml_root): From d80ca33de4da3f12af279b3a458786e2c2f9c65e Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Fri, 12 Aug 2022 09:53:42 -0600 Subject: [PATCH 16/30] Switch to Hatchling --- README.md | 25 +++++++++++++++++++------ pyproject.toml | 14 ++++++++------ src/its_preselector/__init__.py | 4 ---- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4894758..b6c49d4 100644 --- a/README.md +++ b/README.md @@ -139,21 +139,34 @@ pip install .[dev] ``` This will install the project itself, along with development dependencies for pre-commit -hooks, building distributions, and running tests. Set up pre-commit, which runs auto-formatting -and code-checking automatically when you make a commit, by running: +hooks, building distributions, and running tests. Set up pre-commit, which runs +auto-formatting and code-checking automatically when you make a commit, by running: ```bash pre-commit install ``` +The pre-commit tool will auto-format Python code using [Black](https://github.com/psf/black) +and [isort](https://github.com/pycqa/isort). Other pre-commit hooks are also enabled, and +can be found in [`.pre-commit-config.yaml`](.pre-commit-config.yaml). + ### Building New Releases -This project uses [flit](https://github.com/pypa/flit) as a backend. To build a new release -(both wheel and sdist/tarball), first update the version number in -[`src/its_preselector/__init__.py`], then run: +This project uses [Hatchling](https://github.com/pypa/hatch/tree/master/backend) as a backend. +Hatchling makes versioning and building new releases easy. The package version can be updated +easily by using any of the following commands. + +```bash +hatchling version major # 1.0.0 -> 2.0.0 +hatchling version minor # 1.0.0 -> 1.1.0 +hatchling version micro # 1.0.0 -> 1.0.1 +hatchling version "X.X.X" # 1.0.0 -> X.X.X +``` + +To build a new release (both wheel and sdist/tarball), run: ```bash -flit build +hatchling build ``` ## License diff --git a/pyproject.toml b/pyproject.toml index 60a3679..100b851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,11 @@ [build-system] -requires = ["flit_core>=3.4,<4"] -build-backend = "flit_core.buildapi" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "its-preselector" -dynamic = ["version", "description"] +dynamic = ["version"] +description = "A package to control the ITS web relay-based preselector" readme = "README.md" requires-python = ">=3.7" license = { file = "LICENSE.md" } @@ -42,10 +43,11 @@ dependencies = [ [project.optional-dependencies] dev = [ - "flit>=3.4,<4", + "hatchling>=1.6.0,<2.0", "pre-commit>=2.20.0", "pytest>=7.1.2", "pytest-cov>=3.0.0", + "twine>=4.0.1,<5.0" ] [project.urls] @@ -54,5 +56,5 @@ dev = [ "NTIA GitHub" = "https://github.com/NTIA" "ITS Website" = "https://its.ntia.gov" -[tool.flit.module] -name = "its_preselector" +[tool.hatch.version] +path = "src/its_preselector/__init__.py" diff --git a/src/its_preselector/__init__.py b/src/its_preselector/__init__.py index 063a8bc..5becc17 100644 --- a/src/its_preselector/__init__.py +++ b/src/its_preselector/__init__.py @@ -1,5 +1 @@ -"""A package to control the ITS web relay-based preselector - -Refer to the README for more detailed usage information. -""" __version__ = "1.0.0" From 3f01ebb59f25f90c9801f0eacec24bf6d9734484 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Fri, 12 Aug 2022 10:28:54 -0600 Subject: [PATCH 17/30] Add name to controlbyweb_web_relay status. --- src/its_preselector/controlbyweb_web_relay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 735edda..3e00647 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -79,6 +79,7 @@ def get_status(self): except: logger.error('Unable to get status') state['healthy'] = healthy + state['name'] = self.name return state def state_matches(self, relay_and_state, xml_root): From bb6abf584bcb4c4f43b2ff240e92592d80f2befd Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Fri, 12 Aug 2022 11:16:19 -0600 Subject: [PATCH 18/30] Add links to table of contents && git push origin update-backend --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b6c49d4..1104719 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ This software will grow over time to support additional components and control m ## Table of Contents -- Introduction -- Usage -- Development -- License -- Contact +- [Introduction](#introduction) +- [Usage](#usage) +- [Development](#development) +- [License](#license) +- [Contact](#contact) ## Introduction From 9bd52efc1ecaa44e4dfef099145413e5f1b34dc8 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Fri, 12 Aug 2022 14:25:38 -0600 Subject: [PATCH 19/30] debug --- src/its_preselector/controlbyweb_web_relay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 3e00647..f263902 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -65,6 +65,7 @@ def get_status(self): healthy = False try: response = self.get_state_xml() + logger.debug('status code: ' + str(response.status_code)) healthy = response.status_code == requests.codes.ok if healthy: state_xml = response.text From 1cf9515ad709ba9d39a5ec467e48f97bbc5fe209 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 23 Aug 2022 08:02:58 -0600 Subject: [PATCH 20/30] Remove extraneous comma in example config. Add type hints and docstrings. Change rf_paths to dictionary and use rf_path names for interactions. --- README.md | 16 +- config/config.json | 2 +- .../configuration_exception.py | 4 + src/its_preselector/controlbyweb_web_relay.py | 2 +- src/its_preselector/preselector.py | 154 +++++++++++++----- .../test/test_controlbyweb_web_relay.py | 10 +- src/its_preselector/test/test_preselector.py | 41 +++-- src/its_preselector/test/test_rfpaths.py | 8 +- .../test/test_web_relay_preselector.py | 2 +- src/its_preselector/web_relay.py | 40 ++++- src/its_preselector/web_relay_preselector.py | 11 +- 11 files changed, 204 insertions(+), 86 deletions(-) create mode 100644 src/its_preselector/configuration_exception.py diff --git a/README.md b/README.md index ecd189d..9e18388 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,18 @@ for the `WebRelayPreselector` to describe how it works: ```json { - "base_url" : "http://192.168.130.32/state.xml", - "noise_diode_on" : "1State=1,2State=1,3State=0,4State=0", - "noise_diode_off" : "1State=0,2State=1,3State=0,4State=0", - "antenna" : "1State=0,2State=0,3State=0,4State=0" + "name": "preselector", + "base_url" : "http://192.168.1.2/state.xml", + "control_states": { + "noise_diode_on" : "1State=1,2State=1,3State=0,4State=0", + "noise_diode_off" : "1State=1,2State=0,3State=0,4State=0", + "antenna" : "1State=0,2State=0,3State=0,4State=0"}, + "status_states":{ + "noise diode powered" : "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1" + } + } ``` diff --git a/config/config.json b/config/config.json index 83195b5..bd1e978 100644 --- a/config/config.json +++ b/config/config.json @@ -8,7 +8,7 @@ "status_states":{ "noise diode powered" : "relay2=1", "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", + "noise diode path enabled": "relay1=1" } } \ No newline at end of file diff --git a/src/its_preselector/configuration_exception.py b/src/its_preselector/configuration_exception.py new file mode 100644 index 0000000..dc16c1a --- /dev/null +++ b/src/its_preselector/configuration_exception.py @@ -0,0 +1,4 @@ +class ConfigurationException(Exception): + + def __init__(self, message: str): + super().__init__(message) diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index f263902..646eb4b 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -8,7 +8,7 @@ class ControlByWebWebRelay(WebRelay): - def __init__(self, config): + def __init__(self, config: dict): super().__init__(config) if 'base_url' in config: self.base_url = config['base_url'] diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index 5ce5ce5..b43af28 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -1,4 +1,6 @@ -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod + +from its_preselector.configuration_exception import ConfigurationException from its_preselector.rf_path import RfPath from its_preselector.filter import Filter from its_preselector.amplifier import Amplifier @@ -8,9 +10,9 @@ class Preselector(ABC): - def __init__(self, sigmf, config): + def __init__(self, sigmf: dict, config: dict): self.amplifiers = [] - self.rf_paths = [] + self.rf_paths = {} self.filters = [] self.cal_sources = [] self.preselector_spec = [] @@ -60,7 +62,7 @@ def __init__(self, sigmf, config): def __get_rf_paths(self, paths): for path in paths: rf_path = RfPath(path) - self.rf_paths.append(rf_path) + self.rf_paths[rf_path.name] = rf_path def __set_filters(self, filters): for f in filters: @@ -77,62 +79,114 @@ def __set_cal_sources(self, cal_sources): cal_source = CalSource(c) self.cal_sources.append(cal_source) - def get_frequency_low_passband(self, rf_path_index): - if rf_path_index < len(self.rf_paths): - path = self.rf_paths[rf_path_index] + def get_frequency_low_passband(self, rf_path_name: str) -> float: + """ + Get the low frequency of the 1 dB passband. + :param rf_path_name, or name of the rf_path: + :return:The low frequency of hte 1dB passband in Hz. + """ + if rf_path_name in self.rf_paths: + path = self.rf_paths[rf_path_name] filter_id = path.filter_id - preselctor_filter = self.__get_filter(filter_id) - if preselctor_filter: - return preselctor_filter.frequency_low_passband - return None - - def get_frequency_high_passband(self, rf_path_index): - if rf_path_index < len(self.rf_paths): - path = self.rf_paths[rf_path_index] + preselector_filter = self.__get_filter(filter_id) + if preselector_filter: + return preselector_filter.frequency_low_passband + else: + raise ConfigurationException( + "Unable to get frequency_low for the passband filter. There is no RF_PATH named {path_name}".format( + path_name=rf_path_name)) + + def get_frequency_high_passband(self, rf_path_name: str) -> float: + """ + Get the high frequency of the 1 dB passband. + :param rf_path_name: The name of the rf_path. + :return: The high frequency of the 1 dB passband in Hz. + """ + if rf_path_name in self.rf_paths: + path = self.rf_paths[rf_path_name] filter_id = path.filter_id preselector_filter = self.__get_filter(filter_id) if preselector_filter: return preselector_filter.frequency_high_passband - return None - - def get_frequency_low_stopband(self, rf_path_index): - if rf_path_index < len(self.rf_paths): - path = self.rf_paths[rf_path_index] + else: + raise ConfigurationException( + "Unable to get frequency_high for the passband filter. There is no RF_PATH named {path_name}".format( + path_name=rf_path_name)) + + def get_frequency_low_stopband(self, rf_path_name: str): + """ + Gets the low frequency of the 60 dB stopband. + :param rf_path_name: the name of the rf_path. + :return: The low frequency of the 60 dB stopband in Hz. + """ + if rf_path_name in self.rf_paths: + path = self.rf_paths[rf_path_name] filter_id = path.filter_id preselector_filter = self.__get_filter(filter_id) if preselector_filter: return preselector_filter.frequency_low_stopband - return None - - def get_frequency_high_stopband(self, rf_path_index): - if rf_path_index < len(self.rf_paths): - path = self.rf_paths[rf_path_index] + else: + raise ConfigurationException( + "Unable to get frequency_low for the stopband filter. There is no RF_PATH named {path_name}".format( + path_name=rf_path_name)) + + def get_frequency_high_stopband(self, rf_path_name: str) -> float: + """ + Get the high frequency of the 60 dB stopband. + :param rf_path_name: + :return: The high frequency of the 60 dB stopband in Hz. + """ + if rf_path_name in self.rf_paths: + path = self.rf_paths[rf_path_name] filter_id = path.filter_id preselctor_filter = self.__get_filter(filter_id) if preselctor_filter: return preselctor_filter.frequency_high_stopband - return None - - def get_amplifier_gain(self, rf_path_index): - if rf_path_index < len(self.rf_paths): - path = self.rf_paths[rf_path_index] + else: + raise ConfigurationException( + "Unable to get frequency_high for the stopband filter. There is no RF_PATH named {path_name}".format( + path_name=rf_path_name)) + + def get_amplifier_gain(self, rf_path_name: str) -> float: + """ + Get the gain of the amplifier in the specified rf_path. + :param rf_path_name: + :return: + """ + if rf_path_name in self.rf_paths: + path = self.rf_paths[rf_path_name] amp_id = path.amplifier_id amplifier = self.__get_amplifier(amp_id) if amplifier: return amplifier.gain - return None - - def get_amplifier_noise_figure(self, rf_path_index): - if rf_path_index < len(self.rf_paths): - path = self.rf_paths[rf_path_index] + else: + raise ConfigurationException( + "Unable to get amplifier gain. There is no RF_PATH named {path_name}".format( + path_name=rf_path_name)) + + def get_amplifier_noise_figure(self, rf_path_name: str) -> float: + """ + Get the noise figure of the amplifier in the specified rf_path. + :param rf_path_name: The name of the rf_path. + :return: the noise figure of the amplifier in the specified rf_path. + """ + if rf_path_name in self.rf_paths: + path = self.rf_paths[rf_path_name] amp_id = path.amplifier_id amplifier = self.__get_amplifier(amp_id) if amplifier: return amplifier.noise_figure - return None + else: + raise ConfigurationException( + "Unable to get amplifier noise figure. There is no RF_PATH named {path_name}".format( + path_name=rf_path_name)) @abstractmethod - def set_state(self, i): + def set_state(self, state_name: str) -> None: + """ + Sets the state of the preselector. + :param state_name: The name of the state (config key value) to enable in the preselector. + """ pass def __get_filter(self, filter_id): @@ -152,21 +206,39 @@ def __get_amplifier(self, amp_id): return None @abstractmethod - def get_sensor_value(self, sensor): + def get_sensor_value(self, sensor) -> str: + """ + Read the value from a sensor on the preselector. + :param sensor: The name or id of the sensor. + :return: The string value of from the sensor, e.g. the temperature. + """ pass - @property @abstractmethod - def id(self): + def id(self) -> str: + """ + The id of the preselector. + :return: The id of the preselector. + """ pass @property @abstractmethod def name(self) -> str: + """ + Get the name of the preselector. + :return: The name of the preselector. + """ pass @abstractmethod def get_status(self) -> dict: + """ + Get the status of the preselector. The status dictionary should include a name + key value pair indicating the name of hte preselector, a healthy key that maps to + a boolean field to indicate if the preselector is healthy, and any number of additional + keys that map to boolean status values to indicate if the states are enabled or not. + :return: A dictionary representing the status of the preselector. + """ pass - diff --git a/src/its_preselector/test/test_controlbyweb_web_relay.py b/src/its_preselector/test/test_controlbyweb_web_relay.py index 3bef9e2..f009a04 100644 --- a/src/its_preselector/test/test_controlbyweb_web_relay.py +++ b/src/its_preselector/test/test_controlbyweb_web_relay.py @@ -47,7 +47,8 @@ def test_state_matches(self): def test_get_state_from_config(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + web_relay = ControlByWebWebRelay({'name': 'test_preselector', + 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, 'status_states': { "noise diode powered": "relay2=1", "antenna path enabled": "relay1=0", @@ -60,7 +61,7 @@ def test_get_state_from_config(self): type(response).text = PropertyMock(return_value = self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() - self.assertEqual(len(states.keys()), 6) + self.assertEqual(len(states.keys()), 7) self.assertTrue(states['noise diode powered']) self.assertFalse(states['antenna path enabled']) self.assertFalse(states['measurements']) @@ -68,7 +69,8 @@ def test_get_state_from_config(self): self.assertTrue(states['noise on']) def test_get_status(self): - web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, + web_relay = ControlByWebWebRelay({'name': 'test preselector', + 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, 'status_states': { "noise diode powered": "relay2=1", "antenna path enabled": "relay1=0", @@ -81,7 +83,7 @@ def test_get_status(self): type(response).text = PropertyMock(return_value=self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() - self.assertEqual(len(states.keys()), 6) + self.assertEqual(len(states.keys()), 7) self.assertTrue(states['noise diode powered']) self.assertFalse(states['antenna path enabled']) self.assertFalse(states['measurements']) diff --git a/src/its_preselector/test/test_preselector.py b/src/its_preselector/test/test_preselector.py index ca812f7..e79ea77 100644 --- a/src/its_preselector/test/test_preselector.py +++ b/src/its_preselector/test/test_preselector.py @@ -1,4 +1,6 @@ import unittest + +from its_preselector.configuration_exception import ConfigurationException from its_preselector.web_relay_preselector import WebRelayPreselector import json @@ -26,46 +28,51 @@ def test_empty_preselector(self): self.assertIsNotNone(self.empty_preselector) def test_empty_valid_frequency_low_passband(self): - self.assertEqual(3000000000.0, self.preselector.get_frequency_low_passband(0)) - self.assertEqual(self.preselector.get_frequency_low_passband(0), self.preselector.get_frequency_low_passband(1)) + self.assertEqual(3000000000.0, self.preselector.get_frequency_low_passband('noise_diode_on')) + self.assertEqual(self.preselector.get_frequency_low_passband('noise_diode_on'), self.preselector.get_frequency_low_passband('antenna')) def test_empty_get_frequency_low_passband(self): self.assertIsNone(self.empty_preselector.get_frequency_low_passband(0)) def test_valid_get_frequency_high_passband(self): - self.assertEqual(3750000000.0, self.preselector.get_frequency_high_passband(0)) - self.assertEqual(self.preselector.get_frequency_high_passband(0), - self.preselector.get_frequency_high_passband(1)) + self.assertEqual(3750000000.0, self.preselector.get_frequency_high_passband("noise_diode_on")) + self.assertEqual(self.preselector.get_frequency_high_passband("noise_diode_on"), + self.preselector.get_frequency_high_passband("antenna")) def test_empty_get_frequency_high_passband(self): - self.assertIsNone(self.empty_preselector.get_frequency_high_passband(0)) + with self.assertRaises(ConfigurationException): + self.empty_preselector.get_frequency_high_passband("noise_diode_on") def test_valid_get_frequency_low_stopband(self): - self.assertEqual(3550000000.0, self.preselector.get_frequency_low_stopband(0)) - self.assertEqual(self.preselector.get_frequency_low_stopband(0), self.preselector.get_frequency_low_stopband(1)) + self.assertEqual(3550000000.0, self.preselector.get_frequency_low_stopband("noise_diode_on")) + self.assertEqual(self.preselector.get_frequency_low_stopband("noise_diode_on"), self.preselector.get_frequency_low_stopband("antenna")) def test_empty_get_frequency_low_stopband(self): - self.assertIsNone(self.empty_preselector.get_frequency_low_stopband(0)) + with self.assertRaises(ConfigurationException): + self.empty_preselector.get_frequency_low_stopband("noise_diode_on") def test_valid_get_frequency_high_stopband(self): - self.assertEqual(3700000000.0, self.preselector.get_frequency_high_stopband(0)) - self.assertEqual(self.preselector.get_frequency_high_stopband(0), - self.preselector.get_frequency_high_stopband(1)) + self.assertEqual(3700000000.0, self.preselector.get_frequency_high_stopband("noise_diode_on")) + self.assertEqual(self.preselector.get_frequency_high_stopband("noise_diode_on"), + self.preselector.get_frequency_high_stopband("antenna")) def test_empty_get_frequency_high_stopband(self): - self.assertIsNone(self.empty_preselector.get_frequency_high_stopband(0)) + with self.assertRaises(ConfigurationException): + self.empty_preselector.get_frequency_high_stopband("noise_diode_on") def test_get_amplifier_gain(self): - self.assertEqual(30, self.preselector.get_amplifier_gain(0)) + self.assertEqual(30, self.preselector.get_amplifier_gain("noise_diode_on")) def test_empty_get_amplifier_gain(self): - self.assertIsNone(self.empty_preselector.get_amplifier_gain(0)) + with self.assertRaises(ConfigurationException): + self.empty_preselector.get_amplifier_gain(0) def test_get_amplifier_noise_figure(self): - self.assertEqual(2.0, self.preselector.get_amplifier_noise_figure(0)) + self.assertEqual(2.0, self.preselector.get_amplifier_noise_figure("noise_diode_on")) def test_empty_get_amplifier_noise_figure(self): - self.assertIsNone(self.empty_preselector.get_amplifier_noise_figure(0)) + with self.assertRaises(ConfigurationException): + self.empty_preselector.get_amplifier_noise_figure("noise_diode_on") def test_scos_calibration_sources(self): self.assertEqual(1, len(self.scos_preselector.cal_sources)) diff --git a/src/its_preselector/test/test_rfpaths.py b/src/its_preselector/test/test_rfpaths.py index ab22d2b..249e71b 100644 --- a/src/its_preselector/test/test_rfpaths.py +++ b/src/its_preselector/test/test_rfpaths.py @@ -24,16 +24,16 @@ def test_empty_paths(self): self.assertEqual(0, len(self.empty_preselector.rf_paths)) def test_name(self): - self.assertEqual('noise_diode_on', self.preselector.rf_paths[0].name) + self.assertEqual('noise_diode_on', self.preselector.rf_paths["noise_diode_on"].name) def test_cal_source_id(self): - self.assertEqual('SG53400067', self.preselector.rf_paths[0].cal_source_id) + self.assertEqual('SG53400067', self.preselector.rf_paths["noise_diode_on"].cal_source_id) def test_filter_id(self): - self.assertEqual('13FV40, SN 9', self.preselector.rf_paths[0].filter_id) + self.assertEqual('13FV40, SN 9', self.preselector.rf_paths["noise_diode_on"].filter_id) def test_amplifier_id(self): - self.assertEqual('1502150', self.preselector.rf_paths[0].amplifier_id) + self.assertEqual('1502150', self.preselector.rf_paths["noise_diode_on"].amplifier_id) if __name__ == '__main__': diff --git a/src/its_preselector/test/test_web_relay_preselector.py b/src/its_preselector/test/test_web_relay_preselector.py index 45f78e6..0c5a962 100644 --- a/src/its_preselector/test/test_web_relay_preselector.py +++ b/src/its_preselector/test/test_web_relay_preselector.py @@ -58,7 +58,7 @@ def test_get_status_bad_url(self): "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' }}) status = preselector.get_status() - self.assertFalse(status['web_relay_healthy']) + self.assertFalse(status['healthy']) if __name__ == '__main__': diff --git a/src/its_preselector/web_relay.py b/src/its_preselector/web_relay.py index 4f0e5a9..6a56204 100644 --- a/src/its_preselector/web_relay.py +++ b/src/its_preselector/web_relay.py @@ -7,29 +7,57 @@ def __init__(self, config): @abstractmethod - def get_sensor_value(sensor): + def get_sensor_value(sensor: str) -> str: + """ + Read the value from a sensor on the preselector. + :param sensor: The name of the sensor. + :return: The string value read from the sensor, e.g. the temperature. + """ pass @abstractmethod - def set_state(self, i): + def set_state(self, state_key: str) -> None: + """ + Set the state of the web relay as defined in the config. + :param state_key: The key from the config that maps to the desired web relay states. + :return: None + """ pass @abstractmethod - def healthy(self): + def healthy(self) -> bool: + """ + Method to determine if the web relay is healthy. + :return: True if the web relay is healthy. False if it is not. + """ pass @property @abstractmethod - def id(self): + def id(self) -> str: + """ + Get the unique id of hte web relay. + :return: The unique string id of the web relay. + """ pass @property @abstractmethod - def name(self): + def name(self) -> str: + """ + Get the name of the web relay. + :return: The string name of the web relay. + """ pass @abstractmethod - def get_status(self): + def get_status(self) -> dict: + """ + Get the status of the web relay. + :return: A dict describing the status of the web relay. The dict should include a 'name' key that maps to the + name of the web relay, a 'healthy' key that maps to the value returned from healthy(), and any keys defined in + the config that map to boolean values indicating whether or not those states are enabled. + """ pass \ No newline at end of file diff --git a/src/its_preselector/web_relay_preselector.py b/src/its_preselector/web_relay_preselector.py index 1104586..c029ca7 100644 --- a/src/its_preselector/web_relay_preselector.py +++ b/src/its_preselector/web_relay_preselector.py @@ -2,23 +2,22 @@ from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay from its_preselector.preselector import Preselector - logger = logging.getLogger(__name__) class WebRelayPreselector(Preselector): - def __init__(self, sigmf, config): + def __init__(self, sigmf: dict, config: dict): super().__init__(sigmf, config) self.web_relay = ControlByWebWebRelay(config) - def set_state(self, i): + def set_state(self, state_name: str): self.web_relay.set_state(i) - def get_sensor_value(self, sensor_num): + def get_sensor_value(self, sensor_num) -> str: return self.web_relay.get_sensor_value(sensor_num) - def healthy(self): + def healthy(self) -> bool: return self.web_relay.healthy() @property @@ -31,5 +30,3 @@ def name(self): def get_status(self): return self.web_relay.get_status() - - From 2f93eeb38e3791c979ff9d0107303786096a5cd2 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 23 Aug 2022 08:38:55 -0600 Subject: [PATCH 21/30] Require base_url and name in ControlByWebWebRelay and WebRelayPreselector by association. --- README.md | 18 ++++++----- src/its_preselector/controlbyweb_web_relay.py | 15 +++++++++- src/its_preselector/test/test_amplifier.py | 4 +-- src/its_preselector/test/test_cal_source.py | 4 +-- .../test/test_controlbyweb_web_relay.py | 13 ++++---- src/its_preselector/test/test_filter.py | 4 +-- src/its_preselector/test/test_preselector.py | 6 ++-- src/its_preselector/test/test_rfpaths.py | 4 +-- .../test/test_web_relay_preselector.py | 30 +++++++++++++------ src/its_preselector/web_relay_preselector.py | 2 ++ 10 files changed, 67 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 9e18388..fca0c80 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,17 @@ for the `WebRelayPreselector` to describe how it works: } ``` -The `base_url` key is the only required key for the `WebRelayPreselector` and should map to the -base URL to interact with the WebRelay (see [https://www.controlbyweb.com/x310](https://www.controlbyweb.com/x310) -for more info). The other keys should correspond to RF paths documented in the SigMF metadata. -Each of the entries in the config provide mappings to the associated web relay input states and -every RFPath defined in the sensor definition json file should have an entry in the preselector -config. The keys in the dictionary may use the name of the RFPath or the index of the RFPath in -the RFPaths array. +The `base_url` and `name` keys are the only required keys for the `WebRelayPreselector` and should +map to the base URL to interact with the WebRelay +(see [https://www.controlbyweb.com/x310](https://www.controlbyweb.com/x310) +for more info). The keys within the control_states key should correspond to RF paths documented +in the SigMF metadata. The keys within the status_states should map to the RF paths documented in the SigMF metadata, or +to understandable states of the preselector for which it is desired to determine whether they are enabled or +disabled. The status method of the preselector will provide each of the keys specified in the status_states entry mapped +to a boolean indicating whether the preselector states match those specified in the mapping. Each of the entries in the +config provide mappings to the associated web relay input states and every RFPath defined in the sensor definition json +file should have an entry in the preselector config. The keys in the dictionary may use the name of the RFPath or the +index of the RFPath in the RFPaths array. In this example, there are `noise_diode_on` and `noise_diode_off` keys to correspond to the preselector paths to turn the noise diode on and off, and an antenna key to indicate the web diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 646eb4b..165e117 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -1,3 +1,4 @@ +from its_preselector.configuration_exception import ConfigurationException from its_preselector.web_relay import WebRelay import logging import requests @@ -10,8 +11,20 @@ class ControlByWebWebRelay(WebRelay): def __init__(self, config: dict): super().__init__(config) - if 'base_url' in config: + if 'base_url' not in config: + raise ConfigurationException("Config must include base_url.") + elif config['base_url'] is None: + raise ConfigurationException('base_url cannot be None.') + elif config['base_url'] == '': + raise ConfigurationException('base_url cannot be blank.') + else: self.base_url = config['base_url'] + if 'name' not in config: + raise ConfigurationException('Config must include name.') + elif config['name'] is None: + raise ConfigurationException('name cannot be None.') + elif config['name'] == '': + raise ConfigurationException('name cannot be blank.') def get_sensor_value(self, sensor_num): sensor_num_string = str(sensor_num) diff --git a/src/its_preselector/test/test_amplifier.py b/src/its_preselector/test/test_amplifier.py index a4f6802..361bb93 100644 --- a/src/its_preselector/test/test_amplifier.py +++ b/src/its_preselector/test/test_amplifier.py @@ -10,11 +10,11 @@ def setUpClass(cls): file = open('test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() - cls.preselector = WebRelayPreselector(sensor_def, {}) + cls.preselector = WebRelayPreselector(sensor_def, {'base_url': 'http://127.0.0.1', 'name': 'test_preselector'}) null_file = open('null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() - cls.empty_preselector = WebRelayPreselector(null_def, {}) + cls.empty_preselector = WebRelayPreselector(null_def, {'base_url': 'http://127.0.0.1', 'name': 'test_preselector'}) def test_valid_amplifier(self): amplifiers = self.preselector.amplifiers diff --git a/src/its_preselector/test/test_cal_source.py b/src/its_preselector/test/test_cal_source.py index d1fb329..e15af35 100644 --- a/src/its_preselector/test/test_cal_source.py +++ b/src/its_preselector/test/test_cal_source.py @@ -10,11 +10,11 @@ def setUpClass(cls): file = open('test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() - cls.preselector = WebRelayPreselector(sensor_def, {}) + cls.preselector = WebRelayPreselector(sensor_def, {'base_url': 'http://127.0.0.1', 'name': 'test_preselector'}) null_file = open('null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() - cls.empty_preselector = WebRelayPreselector(null_def, {}) + cls.empty_preselector = WebRelayPreselector(null_def, {'base_url': 'http://127.0.0.1', 'name': 'test_preselector'}) def test_valid_cal_source(self): cal_sources = self.preselector.cal_sources diff --git a/src/its_preselector/test/test_controlbyweb_web_relay.py b/src/its_preselector/test/test_controlbyweb_web_relay.py index f009a04..47deb8e 100644 --- a/src/its_preselector/test/test_controlbyweb_web_relay.py +++ b/src/its_preselector/test/test_controlbyweb_web_relay.py @@ -29,7 +29,7 @@ def setUpClass(cls): '''' def test_is_enabled(self): - web_relay = ControlByWebWebRelay({}) + web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', 'name': 'test_switch'}) relay1_enabled = web_relay.is_enabled(self.state, 'relay1') self.assertTrue(relay1_enabled) relay3_enabled = web_relay.is_enabled(self.state, 'relay3') @@ -42,12 +42,14 @@ def test_is_enabled(self): def test_state_matches(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay({'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}}) + web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', 'name': 'test_switch', + 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}}) self.assertTrue(web_relay.state_matches('relay1=1', root)) def test_get_state_from_config(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay({'name': 'test_preselector', + web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', + 'name': 'test_preselector', 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, 'status_states': { "noise diode powered": "relay2=1", @@ -58,7 +60,7 @@ def test_get_state_from_config(self): }}) response = Response() response.status_code = codes.ok - type(response).text = PropertyMock(return_value = self.state) + type(response).text = PropertyMock(return_value=self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() self.assertEqual(len(states.keys()), 7) @@ -69,7 +71,8 @@ def test_get_state_from_config(self): self.assertTrue(states['noise on']) def test_get_status(self): - web_relay = ControlByWebWebRelay({'name': 'test preselector', + web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', + 'name': 'test preselector', 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, 'status_states': { "noise diode powered": "relay2=1", diff --git a/src/its_preselector/test/test_filter.py b/src/its_preselector/test/test_filter.py index bb6100c..b8bee91 100644 --- a/src/its_preselector/test/test_filter.py +++ b/src/its_preselector/test/test_filter.py @@ -10,11 +10,11 @@ def setUpClass(cls): file = open('test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() - cls.preselector = WebRelayPreselector(sensor_def, {}) + cls.preselector = WebRelayPreselector(sensor_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) null_file = open('null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() - cls.empty_preselector = WebRelayPreselector(null_def, {}) + cls.empty_preselector = WebRelayPreselector(null_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) def test_valid_filter_spec(self): spec = self.preselector.filters[0].filter_spec diff --git a/src/its_preselector/test/test_preselector.py b/src/its_preselector/test/test_preselector.py index e79ea77..e9410d6 100644 --- a/src/its_preselector/test/test_preselector.py +++ b/src/its_preselector/test/test_preselector.py @@ -12,14 +12,14 @@ def setUpClass(cls): file = open('test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() - cls.preselector = WebRelayPreselector(sensor_def, {}) + cls.preselector = WebRelayPreselector(sensor_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) null_file = open('null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() - cls.empty_preselector = WebRelayPreselector(null_def, {}) + cls.empty_preselector = WebRelayPreselector(null_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) with open('sensor_definition.json', 'r') as f: sensor_def = json.load(f) - cls.scos_preselector = WebRelayPreselector(sensor_def, {}) + cls.scos_preselector = WebRelayPreselector(sensor_def, {'name': 'scos preselector', 'base_url': 'http://127.0.0.1'}) def test_valid_preselector(self): self.assertIsNotNone(self.preselector) diff --git a/src/its_preselector/test/test_rfpaths.py b/src/its_preselector/test/test_rfpaths.py index 249e71b..1eddbc5 100644 --- a/src/its_preselector/test/test_rfpaths.py +++ b/src/its_preselector/test/test_rfpaths.py @@ -10,11 +10,11 @@ def setUpClass(cls): file = open('test_metadata.sigmf-meta') sensor_def = json.load(file) file.close() - cls.preselector = WebRelayPreselector(sensor_def, {}) + cls.preselector = WebRelayPreselector(sensor_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) null_file = open('null_preselector.sigmf-meta') null_def = json.load(null_file) null_file.close() - cls.empty_preselector = WebRelayPreselector(null_def, {}) + cls.empty_preselector = WebRelayPreselector(null_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) def test_number_valid_paths(self): self.assertEqual(2, len(self.preselector.rf_paths)) diff --git a/src/its_preselector/test/test_web_relay_preselector.py b/src/its_preselector/test/test_web_relay_preselector.py index 0c5a962..34ec9bc 100644 --- a/src/its_preselector/test/test_web_relay_preselector.py +++ b/src/its_preselector/test/test_web_relay_preselector.py @@ -1,6 +1,7 @@ import unittest import json +from its_preselector.configuration_exception import ConfigurationException from its_preselector.web_relay_preselector import WebRelayPreselector @@ -12,20 +13,31 @@ def setUpClass(cls): cls.sensor_def = json.load(file) file.close() + def test_no_name(self): + with self.assertRaises(ConfigurationException): + preselector = WebRelayPreselector(self.sensor_def, + {'base_url': 'http://127.0.0.1', + 'control_states':{'antenna': '1State=0,2State=0,3State=0,4State=0'}}) + + def test_no_base_url(self): + with self.assertRaises(ConfigurationException): + preselector = WebRelayPreselector(self.sensor_def, + {'control_states': {'antenna': '1State=0,2State=0,3State=0,4State=0'}}) + def test_blank_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, - {'base_url': '', 'antenna': '1State=0,2State=0,3State=0,4State=0'}) - with self.assertRaises(Exception): - preselector.set_state('antenna') + with self.assertRaises(ConfigurationException): + preselector = WebRelayPreselector(self.sensor_def, + {'base_url': '', 'name': 'blank url', + 'antenna': '1State=0,2State=0,3State=0,4State=0'}) def test_none_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, - {'base_url': None, 'antenna': '1State=0,2State=0,3State=0,4State=0'}) - with self.assertRaises(Exception): - preselector.set_state('antenna') + with self.assertRaises(ConfigurationException): + preselector = WebRelayPreselector(self.sensor_def, + {'base_url': None, 'name': 'none url', + 'antenna': '1State=0,2State=0,3State=0,4State=0'}) def test_invalid_base_url(self): - preselector = WebRelayPreselector(self.sensor_def, {'base_url': 'http://badpreselector.gov', + preselector = WebRelayPreselector(self.sensor_def, {'name': 'invalid base url', 'base_url': 'http://badpreselector.gov', 'antenna': '1State=0,2State=0,3State=0,4State=0'}) with self.assertRaises(Exception): preselector.set_state('antenna') diff --git a/src/its_preselector/web_relay_preselector.py b/src/its_preselector/web_relay_preselector.py index c029ca7..ba7cf44 100644 --- a/src/its_preselector/web_relay_preselector.py +++ b/src/its_preselector/web_relay_preselector.py @@ -1,4 +1,6 @@ import logging + +from its_preselector.configuration_exception import ConfigurationException from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay from its_preselector.preselector import Preselector From 0ad1079d7c1ae40af142bce59357ac1ccb3a5149 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 23 Aug 2022 12:30:05 -0600 Subject: [PATCH 22/30] Fix readme after merge and increment version. --- README.md | 42 ++++++++++----------------------- src/its_preselector/__init__.py | 2 +- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 594a8e1..e4669a8 100644 --- a/README.md +++ b/README.md @@ -75,37 +75,19 @@ example config file for the `WebRelayPreselector` to describe how it works: } ``` -The `base_url` key is the only required key for the `WebRelayPreselector` and should map -to the base URL to interact with the WebRelay (see -[https://www.controlbyweb.com/x310](https://www.controlbyweb.com/x310) for more info). -The other keys should correspond to RF paths documented in the SigMF metadata. Each of the -entries in the config provide mappings to the associated web relay input states and every -RFPath defined in the sensor definition json file should have an entry in the preselector -config. The keys in the dictionary may use the name of the RFPath or the index of the RFPath -in the RFPaths array. -======= - "antenna" : "1State=0,2State=0,3State=0,4State=0"}, - "status_states":{ - "noise diode powered" : "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1" - } - -} -``` - -The `base_url` and `name` keys are the only required keys for the `WebRelayPreselector` and should -map to the base URL to interact with the WebRelay +The `base_url` and `name` keys are the only required keys for the `WebRelayPreselector`. +The `base_url` should map to the base URL to interact with the WebRelay (see [https://www.controlbyweb.com/x310](https://www.controlbyweb.com/x310) -for more info). The keys within the control_states key should correspond to RF paths documented -in the SigMF metadata. The keys within the status_states should map to the RF paths documented in the SigMF metadata, or -to understandable states of the preselector for which it is desired to determine whether they are enabled or -disabled. The status method of the preselector will provide each of the keys specified in the status_states entry mapped -to a boolean indicating whether the preselector states match those specified in the mapping. Each of the entries in the -config provide mappings to the associated web relay input states and every RFPath defined in the sensor definition json -file should have an entry in the preselector config. The keys in the dictionary may use the name of the RFPath or the -index of the RFPath in the RFPaths array. ->>>>>>> 2f93eeb38e3791c979ff9d0107303786096a5cd2 +for more info). The keys within the control_states key should correspond to RF paths +documented in the SigMF metadata. The keys within the status_states should map to the +RF paths documented in the SigMF metadata, or to understandable states of the +preselector for which it is desired to determine whether they are enabled or disabled. +The status method of the preselector will provide each of the keys specified in the +status_states entry mapped to a boolean indicating whether the preselector states match +those specified in the mapping. Each of the entries in the config provide mappings to the +associated web relay input states and every RFPath defined in the sensor definition json +file should have an entry in the preselector config. The keys in the dictionary may use the +name of the RFPath or the index of the RFPath in the RFPaths array. In this example, there are `noise_diode_on` and `noise_diode_off` keys to correspond to the preselector paths to turn the noise diode on and off, and an antenna key to indicate the diff --git a/src/its_preselector/__init__.py b/src/its_preselector/__init__.py index 5becc17..8c0d5d5 100644 --- a/src/its_preselector/__init__.py +++ b/src/its_preselector/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "2.0.0" From f6e8eb3069e893610a1ecfb479a6a1b2404de5cc Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Tue, 23 Aug 2022 12:47:49 -0600 Subject: [PATCH 23/30] Added --py3-plus arg to pyupgrade --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67568c2..87e1db8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,7 @@ repos: rev: v2.34.0 hooks: - id: pyupgrade + args: ["--py3-upgrade"] - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: From bf34655979e46dedd5902f85bf5a97729a8063e4 Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Tue, 23 Aug 2022 13:23:09 -0600 Subject: [PATCH 24/30] Fix incorrect syntax --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87e1db8..e7722ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: rev: v2.34.0 hooks: - id: pyupgrade - args: ["--py3-upgrade"] + args: ["--py3-plus"] - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: From f3d8e3f807943b060f9eec493b9e62351366b5fb Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Tue, 23 Aug 2022 13:23:59 -0600 Subject: [PATCH 25/30] Fix typo in variable name --- src/its_preselector/preselector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index b43af28..08e09d8 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -139,9 +139,9 @@ def get_frequency_high_stopband(self, rf_path_name: str) -> float: if rf_path_name in self.rf_paths: path = self.rf_paths[rf_path_name] filter_id = path.filter_id - preselctor_filter = self.__get_filter(filter_id) - if preselctor_filter: - return preselctor_filter.frequency_high_stopband + preselector_filter = self.__get_filter(filter_id) + if preselector_filter: + return preselector_filter.frequency_high_stopband else: raise ConfigurationException( "Unable to get frequency_high for the stopband filter. There is no RF_PATH named {path_name}".format( From 03774397354fc9ca6366560685964e4b9a75859d Mon Sep 17 00:00:00 2001 From: Anthony Romaniello Date: Tue, 23 Aug 2022 13:41:42 -0600 Subject: [PATCH 26/30] Fixed indent level --- src/its_preselector/preselector.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index 08e09d8..1f6eedf 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -91,10 +91,10 @@ def get_frequency_low_passband(self, rf_path_name: str) -> float: preselector_filter = self.__get_filter(filter_id) if preselector_filter: return preselector_filter.frequency_low_passband - else: - raise ConfigurationException( - "Unable to get frequency_low for the passband filter. There is no RF_PATH named {path_name}".format( - path_name=rf_path_name)) + else: + raise ConfigurationException( + "Unable to get frequency_low for the passband filter. There is no RF_PATH named {path_name}".format( + path_name=rf_path_name)) def get_frequency_high_passband(self, rf_path_name: str) -> float: """ From 56430e268ca526366e095d17797655a65ceb1ba8 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 23 Aug 2022 14:58:12 -0600 Subject: [PATCH 27/30] Raise additional configuration exceptions. Replace xml parsing with defusedxml. Add additional tests. --- pyproject.toml | 1 + src/its_preselector/controlbyweb_web_relay.py | 97 ++++++------ src/its_preselector/preselector.py | 85 +++++++---- tests/test_controlbyweb_web_relay.py | 141 ++++++++++-------- tests/test_preselector.py | 94 +++++++++--- 5 files changed, 267 insertions(+), 151 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 100b851..43d5806 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ classifiers = [ dependencies = [ "requests>=2.25.1", + "defusedxml>=0.7.1" ] [project.optional-dependencies] diff --git a/src/its_preselector/controlbyweb_web_relay.py b/src/its_preselector/controlbyweb_web_relay.py index 165e117..25e41eb 100644 --- a/src/its_preselector/controlbyweb_web_relay.py +++ b/src/its_preselector/controlbyweb_web_relay.py @@ -1,59 +1,64 @@ -from its_preselector.configuration_exception import ConfigurationException -from its_preselector.web_relay import WebRelay import logging + +import defusedxml.ElementTree as ET import requests -import xml.etree.ElementTree as ET + +from its_preselector.configuration_exception import ConfigurationException +from its_preselector.web_relay import WebRelay logger = logging.getLogger(__name__) class ControlByWebWebRelay(WebRelay): - def __init__(self, config: dict): super().__init__(config) - if 'base_url' not in config: + if "base_url" not in config: raise ConfigurationException("Config must include base_url.") - elif config['base_url'] is None: - raise ConfigurationException('base_url cannot be None.') - elif config['base_url'] == '': - raise ConfigurationException('base_url cannot be blank.') + elif config["base_url"] is None: + raise ConfigurationException("base_url cannot be None.") + elif config["base_url"] == "": + raise ConfigurationException("base_url cannot be blank.") else: - self.base_url = config['base_url'] - if 'name' not in config: - raise ConfigurationException('Config must include name.') - elif config['name'] is None: - raise ConfigurationException('name cannot be None.') - elif config['name'] == '': - raise ConfigurationException('name cannot be blank.') + self.base_url = config["base_url"] + if "name" not in config: + raise ConfigurationException("Config must include name.") + elif config["name"] is None: + raise ConfigurationException("name cannot be None.") + elif config["name"] == "": + raise ConfigurationException("name cannot be blank.") def get_sensor_value(self, sensor_num): sensor_num_string = str(sensor_num) response = requests.get(self.base_url) # Check for X310 xml format first. - sensor_tag = 'sensor' + sensor_num_string + sensor_tag = "sensor" + sensor_num_string root = ET.fromstring(response.text) sensor = root.find(sensor_tag) if sensor is None: # Didn't find X310 format sensor so check for X410 format. - sensor_tag = 'oneWireSensor' + sensor_num_string + sensor_tag = "oneWireSensor" + sensor_num_string sensor = root.find(sensor_tag) if sensor is None: - return None + raise ConfigurationException( + "Sensor {num}".format(num=sensor_num) + " does not exist." + ) else: return sensor.text def set_state(self, key): - if key in self.config['control_states']: - switches = self.config['control_states'][str(key)].split(',') - if self.base_url and self.base_url != '': + if key in self.config["control_states"]: + switches = self.config["control_states"][str(key)].split(",") + if self.base_url and self.base_url != "": for i in range(len(switches)): - command = self.base_url + '?relay' + switches[i] + command = self.base_url + "?relay" + switches[i] logger.debug(command) response = requests.get(command) if response.status_code != requests.codes.ok: - raise Exception('Unable to set preselector state. Verify configuration and connectivity.') + raise Exception( + "Unable to set preselector state. Verify configuration and connectivity." + ) else: - raise Exception('base_url is None or blank') + raise Exception("base_url is None or blank") else: raise Exception("RF path " + key + " configuration does not exist.") @@ -71,46 +76,52 @@ def id(self): @property def name(self): - return self.config['name'] + return self.config["name"] def get_status(self): state = {} healthy = False try: response = self.get_state_xml() - logger.debug('status code: ' + str(response.status_code)) + logger.debug("status code: " + str(response.status_code)) healthy = response.status_code == requests.codes.ok if healthy: state_xml = response.text xml_root = ET.fromstring(state_xml) - for key, value in self.config['status_states'].items(): - relay_states = value.split(',') + for key, value in self.config["status_states"].items(): + relay_states = value.split(",") matches = True for relay_state in relay_states: matches = matches and self.state_matches(relay_state, xml_root) state[key] = matches except: - logger.error('Unable to get status') - state['healthy'] = healthy - state['name'] = self.name + logger.error("Unable to get status") + state["healthy"] = healthy + state["name"] = self.name return state def state_matches(self, relay_and_state, xml_root): - relay_state_list = relay_and_state.split('=') + relay_state_list = relay_and_state.split("=") desired_state = relay_state_list[1] relay_tag = relay_state_list[0] relay_element = xml_root.find(relay_tag) if relay_element is None: - raise Exception('Unable to locate ' + relay_tag) + raise Exception("Unable to locate " + relay_tag) else: return desired_state == relay_element.text def get_state_summary(self, response): - relay_state = '1State=' + self.get_relay_state(response, 'relay1') + \ - ',2State=' + self.get_relay_state(response, 'relay2') + \ - ',3State=' + self.get_relay_state(response, 'relay3') + \ - ',4State=' + self.get_relay_state(response, 'relay4') + relay_state = ( + "1State=" + + self.get_relay_state(response, "relay1") + + ",2State=" + + self.get_relay_state(response, "relay2") + + ",3State=" + + self.get_relay_state(response, "relay3") + + ",4State=" + + self.get_relay_state(response, "relay4") + ) return relay_state def map_relay_state_to_config(self, relay_state): @@ -124,10 +135,10 @@ def is_enabled(state_xml, relay): root = ET.fromstring(state_xml) relay_node = root.find(relay) if relay_node is None: - raise Exception('Relay ' + relay + ' does not exist.') + raise Exception("Relay " + relay + " does not exist.") else: relay_state = relay_node.text - if relay_state == '1': + if relay_state == "1": return True else: return False @@ -137,14 +148,14 @@ def get_relay_state(state_xml, relay): root = ET.fromstring(state_xml) relay_node = root.find(relay) if relay_node is None: - raise Exception('Relay ' + relay + ' does not exist.') + raise Exception("Relay " + relay + " does not exist.") else: relay_state = relay_node.text return relay_state def get_state_xml(self): - if self.base_url and self.base_url != '': + if self.base_url and self.base_url != "": response = requests.get(self.base_url, timeout=1) return response else: - raise Exception('base_url is None or blank') + raise Exception("base_url is None or blank") diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index 1f6eedf..d7518c0 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -1,15 +1,14 @@ from abc import ABC, abstractmethod -from its_preselector.configuration_exception import ConfigurationException -from its_preselector.rf_path import RfPath -from its_preselector.filter import Filter from its_preselector.amplifier import Amplifier from its_preselector.cal_source import CalSource +from its_preselector.configuration_exception import ConfigurationException +from its_preselector.filter import Filter from its_preselector.hardware_spec import HardwareSpec +from its_preselector.rf_path import RfPath class Preselector(ABC): - def __init__(self, sigmf: dict, config: dict): self.amplifiers = [] self.rf_paths = {} @@ -18,44 +17,56 @@ def __init__(self, sigmf: dict, config: dict): self.preselector_spec = [] self.config = config try: - if 'global' in sigmf: - self.__set_filters(sigmf['global']['ntia-sensor:sensor']['preselector']['filters']) + if "global" in sigmf: + self.__set_filters( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["filters"] + ) else: - self.__set_filters(sigmf['preselector']['filters']) + self.__set_filters(sigmf["preselector"]["filters"]) except KeyError: pass try: - if 'global' in sigmf: - self.__set_amplifiers(sigmf['global']['ntia-sensor:sensor']['preselector']['amplifiers']) + if "global" in sigmf: + self.__set_amplifiers( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["amplifiers"] + ) else: - self.__set_amplifiers(sigmf['preselector']['amplifiers']) + self.__set_amplifiers(sigmf["preselector"]["amplifiers"]) except KeyError: pass try: - if 'global' in sigmf: - self.__get_rf_paths(sigmf['global']['ntia-sensor:sensor']['preselector']['rf_paths']) + if "global" in sigmf: + self.__get_rf_paths( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["rf_paths"] + ) else: - self.__get_rf_paths(sigmf['preselector']['rf_paths']) + self.__get_rf_paths(sigmf["preselector"]["rf_paths"]) except KeyError: pass try: - if 'global' in sigmf: - self.__set_cal_sources(sigmf['global']['ntia-sensor:sensor']['preselector']['cal_sources']) + if "global" in sigmf: + self.__set_cal_sources( + sigmf["global"]["ntia-sensor:sensor"]["preselector"]["cal_sources"] + ) else: - self.__set_cal_sources(sigmf['preselector']['cal_sources']) + self.__set_cal_sources(sigmf["preselector"]["cal_sources"]) except KeyError: pass try: - if 'global' in sigmf: + if "global" in sigmf: self.preselector_spec = HardwareSpec( - sigmf['global']['ntia-sensor:sensor']['preselector']['preselector_spec']) + sigmf["global"]["ntia-sensor:sensor"]["preselector"][ + "preselector_spec" + ] + ) else: self.preselector_spec = HardwareSpec( - sigmf['preselector']['preselector_spec']) + sigmf["preselector"]["preselector_spec"] + ) except KeyError: pass @@ -94,7 +105,9 @@ def get_frequency_low_passband(self, rf_path_name: str) -> float: else: raise ConfigurationException( "Unable to get frequency_low for the passband filter. There is no RF_PATH named {path_name}".format( - path_name=rf_path_name)) + path_name=rf_path_name + ) + ) def get_frequency_high_passband(self, rf_path_name: str) -> float: """ @@ -111,7 +124,9 @@ def get_frequency_high_passband(self, rf_path_name: str) -> float: else: raise ConfigurationException( "Unable to get frequency_high for the passband filter. There is no RF_PATH named {path_name}".format( - path_name=rf_path_name)) + path_name=rf_path_name + ) + ) def get_frequency_low_stopband(self, rf_path_name: str): """ @@ -125,10 +140,16 @@ def get_frequency_low_stopband(self, rf_path_name: str): preselector_filter = self.__get_filter(filter_id) if preselector_filter: return preselector_filter.frequency_low_stopband + else: + raise ConfigurationException( + "Filger {id} is None.".format(id=filter_id) + ) else: raise ConfigurationException( "Unable to get frequency_low for the stopband filter. There is no RF_PATH named {path_name}".format( - path_name=rf_path_name)) + path_name=rf_path_name + ) + ) def get_frequency_high_stopband(self, rf_path_name: str) -> float: """ @@ -145,7 +166,9 @@ def get_frequency_high_stopband(self, rf_path_name: str) -> float: else: raise ConfigurationException( "Unable to get frequency_high for the stopband filter. There is no RF_PATH named {path_name}".format( - path_name=rf_path_name)) + path_name=rf_path_name + ) + ) def get_amplifier_gain(self, rf_path_name: str) -> float: """ @@ -159,10 +182,14 @@ def get_amplifier_gain(self, rf_path_name: str) -> float: amplifier = self.__get_amplifier(amp_id) if amplifier: return amplifier.gain + else: + raise ConfigurationException("Amplifier {id} is None".format(amp_id)) else: raise ConfigurationException( "Unable to get amplifier gain. There is no RF_PATH named {path_name}".format( - path_name=rf_path_name)) + path_name=rf_path_name + ) + ) def get_amplifier_noise_figure(self, rf_path_name: str) -> float: """ @@ -176,10 +203,14 @@ def get_amplifier_noise_figure(self, rf_path_name: str) -> float: amplifier = self.__get_amplifier(amp_id) if amplifier: return amplifier.noise_figure + else: + raise ConfigurationException("Amplifier {id} is None".format(id=amp_id)) else: raise ConfigurationException( "Unable to get amplifier noise figure. There is no RF_PATH named {path_name}".format( - path_name=rf_path_name)) + path_name=rf_path_name + ) + ) @abstractmethod def set_state(self, state_name: str) -> None: @@ -195,7 +226,7 @@ def __get_filter(self, filter_id): if f.filter_spec.id == filter_id: return f - return None + raise ConfigurationException("Filter {id} does not exist.".format(id=filter_id)) def __get_amplifier(self, amp_id): if amp_id: @@ -203,7 +234,7 @@ def __get_amplifier(self, amp_id): if amp.amplifier_spec.id == amp_id: return amp - return None + raise ConfigurationException("Amplifier {id} does not exist.".format(id=amp_id)) @abstractmethod def get_sensor_value(self, sensor) -> str: diff --git a/tests/test_controlbyweb_web_relay.py b/tests/test_controlbyweb_web_relay.py index 47deb8e..d57696e 100644 --- a/tests/test_controlbyweb_web_relay.py +++ b/tests/test_controlbyweb_web_relay.py @@ -1,38 +1,42 @@ import unittest +from unittest.mock import MagicMock, PropertyMock + +import defusedxml.ElementTree as ET +from requests import Response, codes + from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay -from requests import codes -from requests import Response -import xml.etree.ElementTree as ET -from unittest.mock import MagicMock -from unittest.mock import PropertyMock class ControlByWebWebRelayTests(unittest.TestCase): - @classmethod def setUpClass(cls): - cls.state = '' \ - '0' \ - '0' \ - '0' \ - '0' \ - '1' \ - '1' \ - '0' \ - '0' \ - '27.6' \ - '0' \ - '102.3' \ - '9160590' \ - '-25200' \ - '00:0C:C8:05:AA:89' \ - '''' + cls.state = ( + "" + "0" + "0" + "0" + "0" + "1" + "1" + "0" + "0" + "27.6" + "0" + "102.3" + "9160590" + "-25200" + "00:0C:C8:05:AA:89" + "" + "" + ) def test_is_enabled(self): - web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', 'name': 'test_switch'}) - relay1_enabled = web_relay.is_enabled(self.state, 'relay1') + web_relay = ControlByWebWebRelay( + {"base_url": "127.0.0.1", "name": "test_switch"} + ) + relay1_enabled = web_relay.is_enabled(self.state, "relay1") self.assertTrue(relay1_enabled) - relay3_enabled = web_relay.is_enabled(self.state, 'relay3') + relay3_enabled = web_relay.is_enabled(self.state, "relay3") self.assertFalse(relay3_enabled) # def test_get_relay_summary(self): @@ -42,57 +46,76 @@ def test_is_enabled(self): def test_state_matches(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', 'name': 'test_switch', - 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}}) - self.assertTrue(web_relay.state_matches('relay1=1', root)) + web_relay = ControlByWebWebRelay( + { + "base_url": "127.0.0.1", + "name": "test_switch", + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + }, + } + ) + self.assertTrue(web_relay.state_matches("relay1=1", root)) def test_get_state_from_config(self): root = ET.fromstring(self.state) - web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', - 'name': 'test_preselector', - 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, - 'status_states': { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": 'relay2=1,relay1=1', - "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' - }}) + web_relay = ControlByWebWebRelay( + { + "base_url": "127.0.0.1", + "name": "test_preselector", + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + }, + "status_states": { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": "relay2=1,relay1=1", + "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", + }, + } + ) response = Response() response.status_code = codes.ok type(response).text = PropertyMock(return_value=self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() self.assertEqual(len(states.keys()), 7) - self.assertTrue(states['noise diode powered']) - self.assertFalse(states['antenna path enabled']) - self.assertFalse(states['measurements']) - self.assertTrue(states['noise diode path enabled']) - self.assertTrue(states['noise on']) + self.assertTrue(states["noise diode powered"]) + self.assertFalse(states["antenna path enabled"]) + self.assertFalse(states["measurements"]) + self.assertTrue(states["noise diode path enabled"]) + self.assertTrue(states["noise on"]) def test_get_status(self): - web_relay = ControlByWebWebRelay({'base_url': '127.0.0.1', - 'name': 'test preselector', - 'control_states': {"noise_diode_off": "1State=1,2State=0,3State=0,4State=0"}, - 'status_states': { - "noise diode powered": "relay2=1", - "antenna path enabled": "relay1=0", - "noise diode path enabled": "relay1=1", - "noise on": 'relay2=1,relay1=1', - "measurements": 'relay1=0,relay2=0,relay3=0,relay4=0' - }}) + web_relay = ControlByWebWebRelay( + { + "base_url": "127.0.0.1", + "name": "test preselector", + "control_states": { + "noise_diode_off": "1State=1,2State=0,3State=0,4State=0" + }, + "status_states": { + "noise diode powered": "relay2=1", + "antenna path enabled": "relay1=0", + "noise diode path enabled": "relay1=1", + "noise on": "relay2=1,relay1=1", + "measurements": "relay1=0,relay2=0,relay3=0,relay4=0", + }, + } + ) response = Response() response.status_code = codes.ok type(response).text = PropertyMock(return_value=self.state) web_relay.get_state_xml = MagicMock(return_value=response) states = web_relay.get_status() self.assertEqual(len(states.keys()), 7) - self.assertTrue(states['noise diode powered']) - self.assertFalse(states['antenna path enabled']) - self.assertFalse(states['measurements']) - self.assertTrue(states['noise diode path enabled']) - self.assertTrue(states['noise on']) + self.assertTrue(states["noise diode powered"]) + self.assertFalse(states["antenna path enabled"]) + self.assertFalse(states["measurements"]) + self.assertTrue(states["noise diode path enabled"]) + self.assertTrue(states["noise on"]) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_preselector.py b/tests/test_preselector.py index 3278b52..b3fba6c 100644 --- a/tests/test_preselector.py +++ b/tests/test_preselector.py @@ -1,27 +1,40 @@ +import json import unittest +from pathlib import Path from its_preselector.configuration_exception import ConfigurationException from its_preselector.web_relay_preselector import WebRelayPreselector -import json -from pathlib import Path class TestWebRelayPreselector(unittest.TestCase): - @classmethod def setUpClass(cls): fpath = Path(__file__).parent.resolve() - file = open(fpath / 'test_metadata.sigmf-meta') + file = open(fpath / "test_metadata.sigmf-meta") sensor_def = json.load(file) file.close() - cls.preselector = WebRelayPreselector(sensor_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) - null_file = open(fpath / 'null_preselector.sigmf-meta') + cls.preselector = WebRelayPreselector( + sensor_def, {"base_url": "127.0.0.1", "name": "test_switch"} + ) + null_file = open(fpath / "null_preselector.sigmf-meta") null_def = json.load(null_file) null_file.close() - cls.empty_preselector = WebRelayPreselector(null_def, {'base_url':'127.0.0.1', 'name': 'test_switch'}) - with open(fpath / 'sensor_definition.json', 'r') as f: + cls.empty_preselector = WebRelayPreselector( + null_def, {"base_url": "127.0.0.1", "name": "test_switch"} + ) + with open(fpath / "sensor_definition.json") as f: sensor_def = json.load(f) - cls.scos_preselector = WebRelayPreselector(sensor_def, {'name': 'scos preselector', 'base_url': 'http://127.0.0.1'}) + cls.scos_preselector = WebRelayPreselector( + sensor_def, {"name": "scos preselector", "base_url": "http://127.0.0.1"} + ) + + def test_requires_name(self): + with self.assertRaises(ConfigurationException): + exception_preselector = WebRelayPreselector({}, {"base_url": "127.0.0.1"}) + + def test_requires_base_url(self): + with self.assertRaises(ConfigurationException): + exception_preselector = WebRelayPreselector({}, {"name": "test_name"}) def test_valid_preselector(self): self.assertIsNotNone(self.preselector) @@ -30,33 +43,64 @@ def test_empty_preselector(self): self.assertIsNotNone(self.empty_preselector) def test_empty_valid_frequency_low_passband(self): - self.assertEqual(3000000000.0, self.preselector.get_frequency_low_passband('noise_diode_on')) - self.assertEqual(self.preselector.get_frequency_low_passband('noise_diode_on'), self.preselector.get_frequency_low_passband('antenna')) + self.assertEqual( + 3000000000.0, self.preselector.get_frequency_low_passband("noise_diode_on") + ) + self.assertEqual( + self.preselector.get_frequency_low_passband("noise_diode_on"), + self.preselector.get_frequency_low_passband("antenna"), + ) def test_empty_get_frequency_low_passband(self): - self.assertIsNone(self.empty_preselector.get_frequency_low_passband(0)) + with self.assertRaises(ConfigurationException): + self.empty_preselector.get_frequency_low_passband("noise_diode_on") + + def test_get_frequency_low_passband_bad_rf_path(self): + with self.assertRaises(ConfigurationException): + self.preselector.get_frequency_low_passband("BadRfPath") def test_valid_get_frequency_high_passband(self): - self.assertEqual(3750000000.0, self.preselector.get_frequency_high_passband("noise_diode_on")) - self.assertEqual(self.preselector.get_frequency_high_passband("noise_diode_on"), - self.preselector.get_frequency_high_passband("antenna")) + self.assertEqual( + 3750000000.0, self.preselector.get_frequency_high_passband("noise_diode_on") + ) + self.assertEqual( + self.preselector.get_frequency_high_passband("noise_diode_on"), + self.preselector.get_frequency_high_passband("antenna"), + ) + + def test_get_frequency_high_passband_bad_rf_path(self): + with self.assertRaises(ConfigurationException): + self.preselector.get_frequency_high_passband("BadRfPath") def test_empty_get_frequency_high_passband(self): with self.assertRaises(ConfigurationException): self.empty_preselector.get_frequency_high_passband("noise_diode_on") def test_valid_get_frequency_low_stopband(self): - self.assertEqual(3550000000.0, self.preselector.get_frequency_low_stopband("noise_diode_on")) - self.assertEqual(self.preselector.get_frequency_low_stopband("noise_diode_on"), self.preselector.get_frequency_low_stopband("antenna")) + self.assertEqual( + 3550000000.0, self.preselector.get_frequency_low_stopband("noise_diode_on") + ) + self.assertEqual( + self.preselector.get_frequency_low_stopband("noise_diode_on"), + self.preselector.get_frequency_low_stopband("antenna"), + ) + + def test_get_frequency_low_stopband_bad_rf_path(self): + with self.assertRaises(ConfigurationException): + self.preselector.get_frequency_low_stopband("BadRfPath") def test_empty_get_frequency_low_stopband(self): with self.assertRaises(ConfigurationException): self.empty_preselector.get_frequency_low_stopband("noise_diode_on") def test_valid_get_frequency_high_stopband(self): - self.assertEqual(3700000000.0, self.preselector.get_frequency_high_stopband("noise_diode_on")) - self.assertEqual(self.preselector.get_frequency_high_stopband("noise_diode_on"), - self.preselector.get_frequency_high_stopband("antenna")) + self.assertEqual( + 3700000000.0, self.preselector.get_frequency_high_stopband("noise_diode_on") + ) + self.assertEqual( + self.preselector.get_frequency_high_stopband("noise_diode_on"), + self.preselector.get_frequency_high_stopband("antenna"), + ) def test_empty_get_frequency_high_stopband(self): with self.assertRaises(ConfigurationException): @@ -65,12 +109,18 @@ def test_empty_get_frequency_high_stopband(self): def test_get_amplifier_gain(self): self.assertEqual(30, self.preselector.get_amplifier_gain("noise_diode_on")) + def test_get_amplifier_gain_nonexistent_amp(self): + with self.assertRaises(ConfigurationException): + self.preselector.get_amplifier_gain("BadAmp") + def test_empty_get_amplifier_gain(self): with self.assertRaises(ConfigurationException): self.empty_preselector.get_amplifier_gain(0) def test_get_amplifier_noise_figure(self): - self.assertEqual(2.0, self.preselector.get_amplifier_noise_figure("noise_diode_on")) + self.assertEqual( + 2.0, self.preselector.get_amplifier_noise_figure("noise_diode_on") + ) def test_empty_get_amplifier_noise_figure(self): with self.assertRaises(ConfigurationException): @@ -80,5 +130,5 @@ def test_scos_calibration_sources(self): self.assertEqual(1, len(self.scos_preselector.cal_sources)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 539a5b5a6b7f75452380d649284539fac55d7eaf Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 23 Aug 2022 15:06:30 -0600 Subject: [PATCH 28/30] Additional ConfigurationException. --- src/its_preselector/preselector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index d7518c0..4a7594e 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -102,6 +102,8 @@ def get_frequency_low_passband(self, rf_path_name: str) -> float: preselector_filter = self.__get_filter(filter_id) if preselector_filter: return preselector_filter.frequency_low_passband + else: + raise ConfigurationException("Filter {id} is None.".format(id=filter)) else: raise ConfigurationException( "Unable to get frequency_low for the passband filter. There is no RF_PATH named {path_name}".format( From 4c547d8f5160ac165a4efeda1b19f89da174f232 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 23 Aug 2022 15:09:48 -0600 Subject: [PATCH 29/30] Additional ConfigurationException. --- src/its_preselector/preselector.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/its_preselector/preselector.py b/src/its_preselector/preselector.py index 4a7594e..90433b8 100644 --- a/src/its_preselector/preselector.py +++ b/src/its_preselector/preselector.py @@ -123,6 +123,10 @@ def get_frequency_high_passband(self, rf_path_name: str) -> float: preselector_filter = self.__get_filter(filter_id) if preselector_filter: return preselector_filter.frequency_high_passband + else: + raise ConfigurationException( + "Filter {id} is None.".format(id=filter_id) + ) else: raise ConfigurationException( "Unable to get frequency_high for the passband filter. There is no RF_PATH named {path_name}".format( From e18d4904f532cd1ba61fa3766e43ea0550581902 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 23 Aug 2022 15:24:05 -0600 Subject: [PATCH 30/30] Corrected renaming. --- src/its_preselector/web_relay_preselector.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/its_preselector/web_relay_preselector.py b/src/its_preselector/web_relay_preselector.py index ba7cf44..1c2c038 100644 --- a/src/its_preselector/web_relay_preselector.py +++ b/src/its_preselector/web_relay_preselector.py @@ -8,13 +8,12 @@ class WebRelayPreselector(Preselector): - def __init__(self, sigmf: dict, config: dict): super().__init__(sigmf, config) self.web_relay = ControlByWebWebRelay(config) def set_state(self, state_name: str): - self.web_relay.set_state(i) + self.web_relay.set_state(state_name) def get_sensor_value(self, sensor_num) -> str: return self.web_relay.get_sensor_value(sensor_num)