Skip to content
Snippets Groups Projects
Commit 84d2cbc0 authored by Hannes Feldt's avatar Hannes Feldt
Browse files

Merge branch 'L2SS-1532-power_hierarchy' into 'master'

L2SS-1532: Allow to determine from which station state a device will be available

Closes L2SS-1532

See merge request !844
parents 304f8dd4 93240878
No related branches found
No related tags found
1 merge request!844L2SS-1532: Allow to determine from which station state a device will be available
Showing with 190 additions and 65 deletions
{
"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": {
......
......@@ -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
......
......@@ -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
0.31.4
0.32.0
......@@ -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(), {})
......
......@@ -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
......
......@@ -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):
"""
......
......@@ -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__),
)
......@@ -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
)
......
......@@ -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(
......
......@@ -12,3 +12,7 @@ class StationStateEnum(IntEnum):
HIBERNATE = 1
STANDBY = 2
ON = 3
@staticmethod
def keys():
return [s.name for s in StationStateEnum]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment