diff --git a/CDB/stations/common.json b/CDB/stations/common.json index 371702e7febce455ab9e64450e04a4630b77d043..c161eecf1376a40bb8e5115b6ed67b1f0f8a5abf 100644 --- a/CDB/stations/common.json +++ b/CDB/stations/common.json @@ -1,4 +1,71 @@ { + "classes": { + "StationManager": { + "properties": { + "Power_Available_In_State": ["HIBERNATE"] + } + }, + "CCD": { + "properties": { + "Power_Available_In_State": ["HIBERNATE"] + } + }, + "PSOC": { + "properties": { + "Power_Available_In_State": ["HIBERNATE"] + } + }, + "PCON": { + "properties": { + "Power_Available_In_State": ["HIBERNATE"] + } + }, + "SDPFirmware": { + "properties": { + "Power_Available_In_State": ["STANDBY"] + } + }, + "TemperatureManager": { + "properties": { + "Power_Available_In_State": ["HIBERNATE"] + } + }, + "APS": { + "properties": { + "Power_Available_In_State": ["STANDBY"] + } + }, + "APSCT": { + "properties": { + "Power_Available_In_State": ["STANDBY"] + } + }, + "APSPU": { + "properties": { + "Power_Available_In_State": ["STANDBY"] + } + }, + "UNB2": { + "properties": { + "Power_Available_In_State": ["STANDBY"] + } + }, + "RECVH": { + "properties": { + "Power_Available_In_State": ["STANDBY"] + } + }, + "RECVL": { + "properties": { + "Power_Available_In_State": ["STANDBY"] + } + }, + "ObservationField": { + "properties": { + "Power_Available_In_State": ["NONE"] + } + } + }, "servers": { "StationManager": { "STAT": { diff --git a/README.md b/README.md index a71e0616be3441a55d66fec30412c2c5d6e92cc8..8d6575fe9ef3e7d476bf6ec14ad411fda000a46f 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,7 @@ Next change the version in the following places: # Release Notes +* 0.32.0 Add available_in_power_state_R attribute to determine from which station state a device will be available * 0.31.4 Bugfixes for DTS configuration, Fixes spurious errors when a station state transition fails Added variables for APS, Calibration, ObservationControl devices to Jupyter notebooks diff --git a/docker/Makefile b/docker/Makefile index 245994c65218f416dd0a34a9e2176e5a9b856361..8c9acf91bb2dbf94caf07d92da0d012880acc7ed 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -38,4 +38,3 @@ ci-runner: integration-test: ci-runner docker build --build-arg SOURCE_IMAGE=$(LOCAL_DOCKER_REGISTRY)/ci-build-runner:$(TAG) -f integration-test/Dockerfile -t $(LOCAL_DOCKER_REGISTRY)/integration-test:$(TAG) integration-test - diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION index a8a0217298153523be41c08773ecf133ba4ce380..9eb2aa3f1095de8c5a2c8c73d33a06f09b323520 100644 --- a/tangostationcontrol/VERSION +++ b/tangostationcontrol/VERSION @@ -1 +1 @@ -0.31.4 +0.32.0 diff --git a/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py b/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py index 1c2b034fc8fbed8102b823421ca1867499e2dec5..8a6426e954510cac6281758e06bc7728d51587f6 100644 --- a/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py +++ b/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py @@ -5,25 +5,73 @@ Power Hierarchy module integration test """ import logging + +import tangostationcontrol +from tango import DevState, DeviceProxy, Database + from integration_test import base from integration_test.device_proxy import TestDeviceProxy -from tango import DevState, DeviceProxy - -from tangostationcontrol.common.constants import N_rcu, N_rcu_inp from tangostationcontrol.common.case_insensitive_string import CaseInsensitiveString +from tangostationcontrol.common.constants import N_rcu, N_rcu_inp from tangostationcontrol.devices.base_device_classes.hierarchy_device import ( NotFoundException, HierarchyMatchingFilter, ) from tangostationcontrol.devices.base_device_classes.power_hierarchy import ( - PowerHierarchyDevice, + PowerHierarchyControlDevice, ) - logger = logging.getLogger() -class TestPowerHierarchyDevice(base.IntegrationTestCase): +class TestPowerAvailableInStateAttribute(base.IntegrationTestCase): + expected = { + "StationManager": "HIBERNATE", + "CCD": "HIBERNATE", + "PSOC": "HIBERNATE", + "PCON": "HIBERNATE", + "SDPFirmware": "STANDBY", + "TemperatureManager": "HIBERNATE", + "APS": "STANDBY", + "APSCT": "STANDBY", + "APSPU": "STANDBY", + "UNB2": "STANDBY", + "RECVH": "STANDBY", + "RECVL": "STANDBY", + "ObservationField": "NONE", + } + + longMessage = True + + def test_power_available_in_state_attribute(self): + db = Database() + + for device in db.get_device_exported("*"): + dev_info = db.get_device_info(device) + if dev_info.class_name == "DServer": + continue + + device_proxy = TestDeviceProxy(device) + if dev_info.class_name not in dir(tangostationcontrol.devices): + logger.warning( + f"device {device} has no attribute available_in_power_state_R" + ) + continue + + available_in_state = device_proxy.available_in_power_state_R + if dev_info.class_name in self.expected: + self.assertEqual( + available_in_state, + self.expected[dev_info.class_name], + f"Device {dev_info.class_name}", + ) + else: + self.assertEqual( + available_in_state, "ON", f"Device {dev_info.class_name}" + ) + + +class TestPowerHierarchyControlDevice(base.IntegrationTestCase): """Integration Test class for PowerHierarchyDevice methods""" pwr_attr_name = "hardware_powered_R" @@ -122,7 +170,7 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): self.setup_stationmanager_proxy() self.setup_all_devices() - stationmanager_ph = PowerHierarchyDevice() + stationmanager_ph = PowerHierarchyControlDevice() stationmanager_ph.init(self.stationmanager_name) children_hierarchy = stationmanager_ph.children(depth=2) @@ -137,7 +185,7 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): ) # Check if EC retrieves correctly its parent state (StationManager -> ON) - ec_ph = PowerHierarchyDevice() + ec_ph = PowerHierarchyControlDevice() ec_ph.init(self.ec_name) self.assertEqual(ec_ph.parent_state(), DevState.ON) # Check if child reads correctly a parent attribute @@ -310,7 +358,7 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): """ self.setup_all_devices() # Create a Hierarchy Device from Device STAT/RECVH/H0 - recvh_ph = PowerHierarchyDevice() + recvh_ph = PowerHierarchyControlDevice() recvh_ph.init(self.recvh_name) self.assertEqual(recvh_ph.parent(), "stat/apspu/h0") # Branch child method must not return its direct parent @@ -338,7 +386,7 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): """ self.setup_all_devices() # Create a Hierarchy Device from Device STAT/RECVH/H0 - recvh_ph = PowerHierarchyDevice() + recvh_ph = PowerHierarchyControlDevice() recvh_ph.init(self.recvh_name) self.assertEqual(recvh_ph.parent(), "stat/apspu/h0") self.assertEqual(recvh_ph.children(), {}) diff --git a/tangostationcontrol/requirements.txt b/tangostationcontrol/requirements.txt index 7cd67a1c16a622ed86ec8370fa27b37872cd629b..f55a555e5024190b192990a0f13ea4a10b58191c 100644 --- a/tangostationcontrol/requirements.txt +++ b/tangostationcontrol/requirements.txt @@ -3,7 +3,7 @@ # integration process, which may cause wedges in the gate later. lofar-station-client@git+https://git.astron.nl/lofar2.0/lofar-station-client # Apache 2 -PyTango>=9.4.2 # LGPL v3 +PyTango>=9.5.1rc1 # LGPL v3 numpy>=1.21.6 # BSD3 asyncua >= 0.9.90 # LGPLv3 psycopg2-binary >= 2.9.2 # LGPL diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py index f89ebdc7162450ddf83bed0edaee2e75830b7c26..bbd7ea309370982a24936be12e7f2ff2b0500abd 100644 --- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py @@ -53,6 +53,9 @@ from tangostationcontrol.common.type_checking import sequence_not_str from tangostationcontrol.devices.base_device_classes.control_hierarchy import ( ControlHierarchyDevice, ) +from tangostationcontrol.devices.base_device_classes.power_hierarchy import ( + power_hierarchy, +) from tangostationcontrol.metrics import device_metrics, AttributeMetric, device_labels __all__ = ["LOFARDevice"] @@ -154,8 +157,10 @@ class AttributePoller: "version_R", "uptime_R", "access_count_R", + "available_in_power_state_R", ] ) +@power_hierarchy class LOFARDevice(Device): """ diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py index a4f76214b723b01f71288d092a90a429334c8678..54f9c3e7519c1d80268fcfdf864d44e411552f0f 100644 --- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py +++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py @@ -7,6 +7,7 @@ from typing import Dict, Optional import logging from tango import DeviceProxy, DevState +from tango.server import attribute, Device, class_property from tangostationcontrol.devices.base_device_classes.hierarchy_device import ( AbstractHierarchyDevice, @@ -18,10 +19,34 @@ from tangostationcontrol.common.device_decorators import suppress_exceptions logger = logging.getLogger() -__all__ = ["PowerHierarchyDevice"] +__all__ = ["PowerHierarchyControlDevice", "power_hierarchy", "power_hierarchy_control"] -class PowerHierarchyDevice(AbstractHierarchyDevice): +class _PowerHierarchyDevice(Device): + """Power Hierarchy""" + + # ----------------- + # Device Properties + # ----------------- + Power_Available_In_State = class_property(dtype="DevString", default_value="ON") + + @attribute(dtype=str) + def available_in_power_state_R(self): + return self.Power_Available_In_State + + +def power_hierarchy(cls): + if not issubclass(cls, Device): + raise TypeError("{cls.__name__} needs to be a subclass of tango.server.Device") + + return type( + cls.__name__, + (cls,), + dict(_PowerHierarchyDevice.__dict__), + ) + + +class PowerHierarchyControlDevice(AbstractHierarchyDevice): """Power Hierarchy""" POWER_CHILD_PROPERTY = "Power_Children" @@ -32,7 +57,9 @@ class PowerHierarchyDevice(AbstractHierarchyDevice): continue_on_failure: bool = False, proxies: Optional[Dict[str, DeviceProxy]] = None, ): - super().init(device_name, self.POWER_CHILD_PROPERTY, proxies) + AbstractHierarchyDevice.init( + self, device_name, self.POWER_CHILD_PROPERTY, proxies + ) self.continue_on_failure = continue_on_failure @@ -50,7 +77,7 @@ class PowerHierarchyDevice(AbstractHierarchyDevice): logger.info("Booting %s: Succesful: state=%s", device, device.state()) def _shutdown_device(self, device: DeviceProxy): - """Default sequence of dervice turning-off operations""" + """Default sequence of device turning-off operations""" if device.state() == DevState.OFF: logger.info("Shutting down %s: Succesful: It's already OFF?", device) return @@ -238,3 +265,14 @@ class PowerHierarchyDevice(AbstractHierarchyDevice): + shutdown_to_standby.exceptions + power_off.exceptions ) + + +def power_hierarchy_control(cls): + return type( + cls.__name__, + ( + cls, + AbstractHierarchyDevice, + ), + dict(PowerHierarchyControlDevice.__dict__), + ) diff --git a/tangostationcontrol/tangostationcontrol/devices/station_manager.py b/tangostationcontrol/tangostationcontrol/devices/station_manager.py index 74fc65f49187e65876f4f4470e48313eb9ef53d6..314ba0fb85a4b00156e6f4c29b5a30145530897c 100644 --- a/tangostationcontrol/tangostationcontrol/devices/station_manager.py +++ b/tangostationcontrol/tangostationcontrol/devices/station_manager.py @@ -19,7 +19,7 @@ from tangostationcontrol.common.lofar_logging import log_exceptions from tangostationcontrol.states.station_state_enum import StationStateEnum from tangostationcontrol.devices.base_device_classes.power_hierarchy import ( - PowerHierarchyDevice, + power_hierarchy_control, ) from tangostationcontrol.devices.base_device_classes.async_device import AsyncDevice from tangostationcontrol.metrics import device_metrics @@ -36,6 +36,7 @@ __all__ = ["StationManager"] "last_requested_transition_exceptions_R", ] ) +@power_hierarchy_control class StationManager(AsyncDevice): """StationManager Device Server for LOFAR2.0""" @@ -97,8 +98,7 @@ class StationManager(AsyncDevice): # overloaded functions # -------- def __init__(self, cl, name): - self.stationmanager_ph = None - self.init_station_state = OffState(self, self.stationmanager_ph) + self.init_station_state = OffState(self, self) self.set_station_state(self.init_station_state) self.requested_station_state = self.init_station_state.state self.last_requested_transition_exceptions = [] @@ -132,7 +132,7 @@ class StationManager(AsyncDevice): self._initialise_power_hierarchy() # Set the station state to off - self.set_station_state(OffState(self, self.stationmanager_ph)) + self.set_station_state(OffState(self, self)) # -------- # internal functions @@ -146,8 +146,7 @@ class StationManager(AsyncDevice): def _initialise_power_hierarchy(self): """Create and initialise the PowerHierarchy to manage the power sequence""" # create a power hierarchy device instance - self.stationmanager_ph = PowerHierarchyDevice() - self.stationmanager_ph.init( + self.init( self.get_name(), continue_on_failure=self.Suppress_State_Transition_Failures ) diff --git a/tangostationcontrol/tangostationcontrol/states/station_state.py b/tangostationcontrol/tangostationcontrol/states/station_state.py index ea36ab0c20ae9c4ed1b93824703361fbb098ca9c..b13773aec3612b6b6486cd3dbfbc47e4453d0eab 100644 --- a/tangostationcontrol/tangostationcontrol/states/station_state.py +++ b/tangostationcontrol/tangostationcontrol/states/station_state.py @@ -3,65 +3,31 @@ import asyncio import logging from abc import ABC, abstractmethod -from typing import Callable, Awaitable, Dict, Optional +from typing import Callable, Awaitable from functools import wraps from tango import DevState, DeviceProxy from tangostationcontrol.states.station_state_enum import StationStateEnum -from tangostationcontrol.common.type_checking import device_class_matches __all__ = ["StationState"] logger = logging.getLogger() -DEVICES_ON_IN_STATION_STATE: Dict[str, Optional[StationStateEnum]] = { - """In which StationState each device class should be switched ON.""" - "StationManager": StationStateEnum.HIBERNATE, - "CCD": StationStateEnum.HIBERNATE, - "EC": StationStateEnum.HIBERNATE, - "PCON": StationStateEnum.HIBERNATE, - "PSOC": StationStateEnum.HIBERNATE, - "TemperatureManager": StationStateEnum.HIBERNATE, - "Docker": StationStateEnum.HIBERNATE, - "Configuration": StationStateEnum.HIBERNATE, - "SDPFirmware": StationStateEnum.STANDBY, - "APS": StationStateEnum.STANDBY, - "APSPU": StationStateEnum.STANDBY, - "APSCT": StationStateEnum.STANDBY, - "UNB2": StationStateEnum.STANDBY, - "RECVH": StationStateEnum.STANDBY, - "RECVL": StationStateEnum.STANDBY, - # TODO(JDM) Lingering observations (and debug observation 0) - # should not be booted as we do not persist their settings - "ObservationField": None, - # Unmentioned devices will go ON in this state - "_default": StationStateEnum.ON, -} - - def run_if_device_on_in_station_state( target_state: StationStateEnum, - state_table: Dict[str, StationStateEnum] = None, ): """Decorator for a function func(device). It only executes the decorated function if the device should be turned ON in the target_state. Returns None otherwise.""" - if state_table is None: - state_table = DEVICES_ON_IN_STATION_STATE - def inner(func): @wraps(func) def wrapper(device: DeviceProxy, *args, **kwargs): def should_execute(): - for pattern, station_state in state_table.items(): - if not device_class_matches(device, pattern): - continue - - return target_state == station_state - - # no matches, use default (if any) - return target_state == state_table.get("_default") + available_in_state = device.available_in_power_state_R + if available_in_state in StationStateEnum.keys(): + return target_state == StationStateEnum[available_in_state] + return False # execute function if we should in the configured state return func(device, *args, **kwargs) if should_execute() else None @@ -140,9 +106,8 @@ class StationState(ABC): ): """Transition to a station state using `transition_func`. - :param transition_state : transition to be performed + :param target_state : transition to be performed :param transition_func : function that implements the transition - :param state_timeout: timeout of transition to be performed """ # StationManager device must be in ON state @@ -204,7 +169,6 @@ class StationState(ABC): :param target_state : transition to be performed :param pwr_func : power hierarchy function that implements the transition - :param timeout: timeout of transition to be performed """ try: return await asyncio.wait_for( diff --git a/tangostationcontrol/tangostationcontrol/states/station_state_enum.py b/tangostationcontrol/tangostationcontrol/states/station_state_enum.py index 6a40cf204dc8f0a7dadd80b44026d7a805d2758f..48f905b47ebfc7938deeaaf59a788da224f86c99 100644 --- a/tangostationcontrol/tangostationcontrol/states/station_state_enum.py +++ b/tangostationcontrol/tangostationcontrol/states/station_state_enum.py @@ -12,3 +12,7 @@ class StationStateEnum(IntEnum): HIBERNATE = 1 STANDBY = 2 ON = 3 + + @staticmethod + def keys(): + return [s.name for s in StationStateEnum]