From 8ee8c6410e2ac3e2b890f0fa8658eeb24dcfb417 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Mon, 22 Jan 2024 12:38:32 -0700 Subject: [PATCH] cleanup --- src/initialization/__init__.py | 26 ++-- src/initialization/action_loader.py | 65 ++++++--- src/initialization/apps.py | 7 + src/initialization/capabilities_loader.py | 9 +- src/initialization/sensor_loader.py | 155 ++++++++++++++-------- src/initialization/status_monitor.py | 24 +++- 6 files changed, 189 insertions(+), 97 deletions(-) diff --git a/src/initialization/__init__.py b/src/initialization/__init__.py index 6ad8c256..d0e8e320 100644 --- a/src/initialization/__init__.py +++ b/src/initialization/__init__.py @@ -18,7 +18,7 @@ status_monitor = StatusMonitor() -def usb_exists() -> bool: +def usb_device_exists() -> bool: logger.debug("Checking for USB...") if settings.USB_DEVICE is not None: usb_devices = check_output("lsusb").decode(sys.stdout.encoding) @@ -36,22 +36,27 @@ def status_registration_handler(sender, **kwargs): logger.exception("Error registering status component") +def set_container_unhealthy(): + if settings.IN_DOCKER: + logger.warning("Signal analyzer is not healthy. Marking container for restart.") + Path(settings.SDR_HEALTHCHECK_FILE).touch() + + try: register_component_with_status.connect(status_registration_handler) - usb_exists = usb_exists() - if usb_exists: + usb_device_exists = usb_device_exists() + if usb_device_exists: action_loader = ActionLoader() logger.debug("test") logger.debug(f"Actions ActionLoader has {len(action_loader.actions)} actions") capabilities_loader = CapabilitiesLoader() logger.debug("Calling sensor loader.") sensor_loader = SensorLoader(capabilities_loader.capabilities) - if not sensor_loader.sensor.signal_analyzer.healthy(): - if settings.IN_DOCKER: - logger.warning( - "Signal analyzer is not healthy. Marking container for restart." - ) - Path(settings.SDR_HEALTHCHECK_FILE).touch() + if ( + not settings.RUNNING_MIGRATIONS + and not sensor_loader.sensor.signal_analyzer.healthy() + ): + set_container_unhealthy() else: action_loader = types.SimpleNamespace() action_loader.actions = {} @@ -64,7 +69,6 @@ def status_registration_handler(sender, **kwargs): sensor_loader.switches = {} sensor_loader.capabilities = {} logger.warning("Usb is not ready. Marking container as unhealthy") - if settings.IN_DOCKER: - Path(settings.SDR_HEALTHCHECK_FILE).touch() + set_container_unhealthy() except Exception as ex: logger.error(f"Error during initialization: {ex}") diff --git a/src/initialization/action_loader.py b/src/initialization/action_loader.py index fa3289d8..d58fdd6b 100644 --- a/src/initialization/action_loader.py +++ b/src/initialization/action_loader.py @@ -4,32 +4,56 @@ import os import pkgutil import shutil +from typing import Dict + from django.conf import settings from scos_actions.actions import action_classes -from scos_actions.discover import test_actions -from scos_actions.discover import init +from scos_actions.discover import init, test_actions +from scos_actions.interfaces.action import Action logger = logging.getLogger(__name__) -class ActionLoader(object): + +class ActionLoader: + """ + Loads actions from scos_ plugins and any yaml configurations + in the configs/actions directory. Note: this class is a + singleton so other applications may safely create an instance + and reference the .actions property. + """ + _instance = None def __init__(self): if not hasattr(self, "actions"): logger.debug("Actions have not been loaded. Loading actions...") - self.actions = load_actions_and_sigan(settings.MOCK_SIGAN, settings.RUNNING_TESTS, settings.DRIVERS_DIR, - settings.ACTIONS_DIR) + self._actions = load_actions( + settings.MOCK_SIGAN, + settings.RUNNING_TESTS, + settings.DRIVERS_DIR, + settings.ACTIONS_DIR, + ) else: logger.debug("Already loaded actions. ") def __new__(cls): if cls._instance is None: - logger.debug('Creating the ActionLoader') - cls._instance = super(ActionLoader, cls).__new__(cls) - logger.debug(f"Calling load_actions with {settings.MOCK_SIGAN}, {settings.RUNNING_TESTS}, {settings.DRIVERS_DIR}, {settings.ACTIONS_DIR}") + logger.debug("Creating the ActionLoader") + cls._instance = super().__new__(cls) + logger.debug( + f"Calling load_actions with {settings.MOCK_SIGAN}, {settings.RUNNING_TESTS}, {settings.DRIVERS_DIR}, {settings.ACTIONS_DIR}" + ) return cls._instance -def copy_driver_files(driver_dir): + @property + def actions(self) -> Dict[str, Action]: + """ + Returns all sensor actions configured in the system. + """ + return self._actions + + +def copy_driver_files(driver_dir: str): """Copy driver files where they need to go""" logger.debug(f"Copying driver files in {driver_dir}") for root, dirs, files in os.walk(driver_dir): @@ -43,9 +67,7 @@ def copy_driver_files(driver_dir): if type(json_data) == dict and "scos_files" in json_data: scos_files = json_data["scos_files"] for scos_file in scos_files: - source_path = os.path.join( - driver_dir, scos_file["source_path"] - ) + source_path = os.path.join(driver_dir, scos_file["source_path"]) if not os.path.isfile(source_path): logger.error(f"Unable to find file at {source_path}") continue @@ -60,8 +82,10 @@ def copy_driver_files(driver_dir): logger.error(f"Failed to copy {source_path} to {dest_path}") logger.error(e) -def load_actions_and_sigan(mock_sigan, running_tests, driver_dir, action_dir): +def load_actions( + mock_sigan: bool, running_tests: bool, driver_dir: str, action_dir: str +): logger.debug("********** Initializing actions **********") copy_driver_files(driver_dir) # copy driver files before loading plugins discovered_plugins = { @@ -71,7 +95,6 @@ def load_actions_and_sigan(mock_sigan, running_tests, driver_dir, action_dir): } logger.debug(discovered_plugins) actions = {} - signal_analyzer = None if mock_sigan or running_tests: logger.debug(f"Loading {len(test_actions)} test actions.") actions.update(test_actions) @@ -82,16 +105,16 @@ def load_actions_and_sigan(mock_sigan, running_tests, driver_dir, action_dir): if hasattr(discover, "actions"): logger.debug(f"loading {len(discover.actions)} actions.") actions.update(discover.actions) - if hasattr(discover, "action_classes") and discover.action_classes is not None: + if ( + hasattr(discover, "action_classes") + and discover.action_classes is not None + ): action_classes.update(discover.action_classes) - # if hasattr(discover, "signal_analyzer") and discover.signal_analyzer is not None: - # logger.debug(f"Found signal_analyzer: {discover.signal_analyzer}") - # signal_analyzer = discover.signal_analyzer - # else: - # logger.debug(f"{discover} has no signal_analyzer attribute") logger.debug(f"Loading actions in {action_dir}") - yaml_actions, yaml_test_actions = init(action_classes=action_classes, yaml_dir=action_dir) + yaml_actions, yaml_test_actions = init( + action_classes=action_classes, yaml_dir=action_dir + ) actions.update(yaml_actions) logger.debug("Finished loading and registering actions") return actions diff --git a/src/initialization/apps.py b/src/initialization/apps.py index 46e8f1e4..6dc552bc 100644 --- a/src/initialization/apps.py +++ b/src/initialization/apps.py @@ -2,4 +2,11 @@ class InitializationConfig(AppConfig): + """ + The first application to load. This application is responsible + for initializing the hardware components and loading actions. + This ensures the components are initialized in the appropriate + order and available for the other applications. + """ + name = "initialization" diff --git a/src/initialization/capabilities_loader.py b/src/initialization/capabilities_loader.py index 4db42640..4c8d2059 100644 --- a/src/initialization/capabilities_loader.py +++ b/src/initialization/capabilities_loader.py @@ -1,6 +1,7 @@ import hashlib import json import logging + from django.conf import settings from scos_actions.utils import load_from_json @@ -13,7 +14,7 @@ class CapabilitiesLoader: def __init__(self): if not hasattr(self, "capabilities"): logger.debug("Capabilities have not been loaded. Loading...") - self.capabilities = load_capabilities(settings.SENSOR_DEFINITION_FILE) + self._capabilities = load_capabilities(settings.SENSOR_DEFINITION_FILE) else: logger.debug("Already loaded capabilities. ") @@ -23,8 +24,12 @@ def __new__(cls): cls._instance = super().__new__(cls) return cls._instance + @property + def capabilities(self) -> dict: + return self._capabilities + -def load_capabilities(sensor_definition_file) -> dict: +def load_capabilities(sensor_definition_file: str) -> dict: capabilities = {} sensor_definition_hash = None sensor_location = None diff --git a/src/initialization/sensor_loader.py b/src/initialization/sensor_loader.py index 538ec101..1cb86e91 100644 --- a/src/initialization/sensor_loader.py +++ b/src/initialization/sensor_loader.py @@ -1,41 +1,49 @@ import importlib import logging import signal -from django.conf import settings -from scos_actions.hardware.sensor import Sensor -from scos_actions.hardware.sigan_iface import SignalAnalyzerInterface -from scos_actions.metadata.utils import construct_geojson_point from os import path from pathlib import Path + +from django.conf import settings from its_preselector.configuration_exception import ConfigurationException from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay +from its_preselector.preselector import Preselector from scos_actions import utils from scos_actions.calibration.calibration import Calibration, load_from_json +from scos_actions.hardware.sensor import Sensor +from scos_actions.metadata.utils import construct_geojson_point + from utils.signals import register_component_with_status logger = logging.getLogger(__name__) -class SensorLoader(object): + +class SensorLoader: _instance = None - def __init__(self, sensor_capabilities): + def __init__(self, sensor_capabilities: dict): if not hasattr(self, "sensor"): logger.debug("Sensor has not been loaded. Loading...") - self.sensor = load_sensor(sensor_capabilities) + self._sensor = load_sensor(sensor_capabilities) else: logger.debug("Already loaded sensor. ") def __new__(cls, sensor_capabilities): if cls._instance is None: - logger.debug('Creating the SensorLoader') - cls._instance = super(SensorLoader, cls).__new__(cls) + logger.debug("Creating the SensorLoader") + cls._instance = super().__new__(cls) return cls._instance -def load_sensor(sensor_capabilities): + @property + def sensor(self) -> Sensor: + return self._sensor + + +def load_sensor(sensor_capabilities: dict) -> Sensor: location = None - #Remove location from sensor definition and convert to geojson. - #Db may have an updated location, but status module will update it - #if needed. + # Remove location from sensor definition and convert to geojson. + # Db may have an updated location, but status module will update it + # if needed. if "location" in sensor_capabilities["sensor"]: sensor_loc = sensor_capabilities["sensor"].pop("location") location = construct_geojson_point( @@ -44,54 +52,72 @@ def load_sensor(sensor_capabilities): sensor_loc["z"] if "z" in sensor_loc else None, ) switches = load_switches(settings.SWITCH_CONFIGS_DIR) - sensor_cal = get_sensor_calibration(settings.SENSOR_CALIBRATION_FILE, settings.DEFAULT_CALIBRATION_FILE) - sigan_cal = get_sigan_calibration(settings.SIGAN_CALIBRATION_FILE, settings.DEFAULT_CALIBRATION_FILE) + sensor_cal = get_sensor_calibration( + settings.SENSOR_CALIBRATION_FILE, settings.DEFAULT_CALIBRATION_FILE + ) + sigan_cal = get_sigan_calibration( + settings.SIGAN_CALIBRATION_FILE, settings.DEFAULT_CALIBRATION_FILE + ) sigan = None try: if not settings.RUNNING_MIGRATIONS: sigan_module_setting = settings.SIGAN_MODULE sigan_module = importlib.import_module(sigan_module_setting) - logger.info("Creating " + settings.SIGAN_CLASS + " from " + settings.SIGAN_MODULE) + logger.info( + "Creating " + settings.SIGAN_CLASS + " from " + settings.SIGAN_MODULE + ) sigan_constructor = getattr(sigan_module, settings.SIGAN_CLASS) - sigan = sigan_constructor(sensor_cal=sensor_cal, sigan_cal=sigan_cal, switches = switches) + sigan = sigan_constructor( + sensor_cal=sensor_cal, sigan_cal=sigan_cal, switches=switches + ) register_component_with_status.send(sigan, component=sigan) else: logger.info("Running migrations. Not loading signal analyzer.") except Exception as ex: logger.warning(f"unable to create signal analyzer: {ex}") - preselector = load_preselector(settings.PRESELECTOR_CONFIG, settings.PRESELECTOR_MODULE, - settings.PRESELECTOR_CLASS, sensor_capabilities["sensor"]) - - sensor = Sensor(signal_analyzer=sigan, preselector=preselector, switches=switches, capabilities=sensor_capabilities, - location=location) + preselector = load_preselector( + settings.PRESELECTOR_CONFIG, + settings.PRESELECTOR_MODULE, + settings.PRESELECTOR_CLASS, + sensor_capabilities["sensor"], + ) + + sensor = Sensor( + signal_analyzer=sigan, + preselector=preselector, + switches=switches, + capabilities=sensor_capabilities, + location=location, + ) return sensor - def load_switches(switch_dir: Path) -> dict: - logger.debug(f"Loading switches in {switch_dir}") - switch_dict = {} - try: - if switch_dir is not None and switch_dir.is_dir(): - for f in switch_dir.iterdir(): - file_path = f.resolve() - logger.debug(f"loading switch config {file_path}") - conf = utils.load_from_json(file_path) - try: - switch = ControlByWebWebRelay(conf) - logger.debug(f"Adding {switch.id}") - switch_dict[switch.id] = switch - logger.debug(f"Registering switch status for {switch.name}") - register_component_with_status.send(__name__, component=switch) - except ConfigurationException: - logger.error(f"Unable to configure switch defined in: {file_path}") - except Exception as ex: - logger.error(f"Unable to load switches {ex}") - return switch_dict - - -def load_preselector_from_file(preselector_module, preselector_class, preselector_config_file: Path): + logger.debug(f"Loading switches in {switch_dir}") + switch_dict = {} + try: + if switch_dir is not None and switch_dir.is_dir(): + for f in switch_dir.iterdir(): + file_path = f.resolve() + logger.debug(f"loading switch config {file_path}") + conf = utils.load_from_json(file_path) + try: + switch = ControlByWebWebRelay(conf) + logger.debug(f"Adding {switch.id}") + switch_dict[switch.id] = switch + logger.debug(f"Registering switch status for {switch.name}") + register_component_with_status.send(__name__, component=switch) + except ConfigurationException: + logger.error(f"Unable to configure switch defined in: {file_path}") + except Exception as ex: + logger.error(f"Unable to load switches {ex}") + return switch_dict + + +def load_preselector_from_file( + preselector_module, preselector_class, preselector_config_file: Path +): if preselector_config_file is None: return None else: @@ -107,8 +133,15 @@ def load_preselector_from_file(preselector_module, preselector_class, preselecto return None -def load_preselector(preselector_config: str, module: str, preselector_class_name: str, sensor_definition: dict): - logger.debug(f"loading {preselector_class_name} from {module} with config: {preselector_config}") +def load_preselector( + preselector_config: str, + module: str, + preselector_class_name: str, + sensor_definition: dict, +) -> Preselector: + logger.debug( + f"loading {preselector_class_name} from {module} with config: {preselector_config}" + ) if module is not None and preselector_class_name is not None: preselector_module = importlib.import_module(module) preselector_constructor = getattr(preselector_module, preselector_class_name) @@ -120,10 +153,9 @@ def load_preselector(preselector_config: str, module: str, preselector_class_nam return ps - - - -def get_sigan_calibration(sigan_cal_file_path: str, default_cal_file_path: str) -> Calibration: +def get_sigan_calibration( + sigan_cal_file_path: str, default_cal_file_path: str +) -> Calibration: """ Load signal analyzer calibration data from file. @@ -135,14 +167,19 @@ def get_sigan_calibration(sigan_cal_file_path: str, default_cal_file_path: str) try: sigan_cal = None if sigan_cal_file_path is None or sigan_cal_file_path == "": - logger.warning("No sigan calibration file specified. Not loading calibration file.") + logger.warning( + "No sigan calibration file specified. Not loading calibration file." + ) elif not path.exists(sigan_cal_file_path): logger.warning( - sigan_cal_file_path + " does not exist. Not loading sigan calibration file." + sigan_cal_file_path + + " does not exist. Not loading sigan calibration file." ) else: logger.debug(f"Loading sigan cal file: {sigan_cal_file_path}") - default = check_for_default_calibration(sigan_cal_file_path,default_cal_file_path, "Sigan") + default = check_for_default_calibration( + sigan_cal_file_path, default_cal_file_path, "Sigan" + ) sigan_cal = load_from_json(sigan_cal_file_path, default) sigan_cal.is_default = default except Exception: @@ -151,7 +188,9 @@ def get_sigan_calibration(sigan_cal_file_path: str, default_cal_file_path: str) return sigan_cal -def get_sensor_calibration(sensor_cal_file_path: str, default_cal_file_path: str) -> Calibration: +def get_sensor_calibration( + sensor_cal_file_path: str, default_cal_file_path: str +) -> Calibration: """ Load sensor calibration data from file. @@ -184,11 +223,13 @@ def get_sensor_calibration(sensor_cal_file_path: str, default_cal_file_path: str return sensor_cal -def check_for_default_calibration(cal_file_path: str,default_cal_path: str, cal_type: str) -> bool: +def check_for_default_calibration( + cal_file_path: str, default_cal_path: str, cal_type: str +) -> bool: default_cal = False if cal_file_path == default_cal_path: default_cal = True logger.warning( f"***************LOADING DEFAULT {cal_type} CALIBRATION***************" ) - return default_cal \ No newline at end of file + return default_cal diff --git a/src/initialization/status_monitor.py b/src/initialization/status_monitor.py index 611d62d9..64340c79 100644 --- a/src/initialization/status_monitor.py +++ b/src/initialization/status_monitor.py @@ -2,16 +2,29 @@ logger = logging.getLogger(__name__) -class StatusMonitor(object): + +class StatusMonitor: + """ + Singleton the keeps track of all components within the system that can provide + status. + """ + _instance = None def __new__(cls): if cls._instance is None: - logger.debug('Creating the ActionLoader') - cls._instance = super(StatusMonitor, cls).__new__(cls) - cls._instance.status_components = [] + logger.debug("Creating the ActionLoader") + cls._instance = super().__new__(cls) + cls._instance._status_components = [] return cls._instance + @property + def status_components(self): + """ + Returns any components that have been registered as status providing. + """ + return self._status_components + def add_component(self, component): """ Allows objects to be registered to provide status. Any object registered will @@ -21,5 +34,4 @@ def add_component(self, component): :param component: the object to add to the list of status providing objects. """ if hasattr(component, "get_status"): - self.status_components.append(component) - + self._status_components.append(component)