diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9c5709f69edfef2b5afb242aff07b5ed4c00988e..53fece743632ac17d0e75d830406eacbb2627117 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -369,6 +369,9 @@ deploy_nomad: image: name: hashicorp/levant entrypoint: [ "" ] + needs: + - docker_build_image + - docker_build_image_device_base when: manual rules: - if: ($CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) || $CI_COMMIT_TAG 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 721a512468a8b85d4d56ac6e229cf32c164e73fc..1c2b034fc8fbed8102b823421ca1867499e2dec5 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 @@ -153,8 +153,9 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): self.assertEqual(self.ccd_proxy.state(), DevState.OFF) # Switch from OFF to HIBERNATE self.stationmanager_proxy.station_hibernate() + self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE") self.assertEqual( - self.stationmanager_proxy.last_requested_transition_R, "OFF -> HIBERNATE" + self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE" ) self.assertEqual(self.psoc_proxy.state(), DevState.ON) self.assertEqual(self.pcon_proxy.state(), DevState.ON) @@ -173,8 +174,9 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): """ # Switch from OFF to HIBERNATE self.stationmanager_proxy.station_hibernate() + self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE") self.assertEqual( - self.stationmanager_proxy.last_requested_transition_R, "OFF -> HIBERNATE" + self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE" ) self.assertEqual(self.apspu_h0_proxy.state(), DevState.OFF) self.assertEqual(self.apspu_l0_proxy.state(), DevState.OFF) @@ -186,9 +188,9 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): self.assertEqual(self.sdpfirmware_proxy.state(), DevState.OFF) # Switch from HIBERNATE to STANDBY self.stationmanager_proxy.station_standby() + self.assertEqual(self.stationmanager_proxy.station_state_R.name, "STANDBY") self.assertEqual( - self.stationmanager_proxy.last_requested_transition_R, - "HIBERNATE -> STANDBY", + self.stationmanager_proxy.requested_station_state_R.name, "STANDBY" ) self.assertEqual(self.apspu_h0_proxy.state(), DevState.ON) self.assertEqual(self.apspu_l0_proxy.state(), DevState.ON) @@ -224,9 +226,8 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): self.assertEqual(self.sdp_proxy.state(), DevState.OFF) self.assertEqual(self.antennafield_proxy.state(), DevState.OFF) self.stationmanager_proxy.station_on() - self.assertEqual( - self.stationmanager_proxy.last_requested_transition_R, "STANDBY -> ON" - ) + self.assertEqual(self.stationmanager_proxy.station_state_R.name, "ON") + self.assertEqual(self.stationmanager_proxy.requested_station_state_R.name, "ON") self.assertEqual(self.sdp_proxy.state(), DevState.ON) self.assertEqual(self.antennafield_proxy.state(), DevState.ON) @@ -259,8 +260,9 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): self.stationmanager_proxy.station_on() # Reverse to STANDBY self.stationmanager_proxy.station_standby() + self.assertEqual(self.stationmanager_proxy.station_state_R.name, "STANDBY") self.assertEqual( - self.stationmanager_proxy.last_requested_transition_R, "ON -> STANDBY" + self.stationmanager_proxy.requested_station_state_R.name, "STANDBY" ) self.assertEqual(self.sdp_proxy.state(), DevState.OFF) self.assertEqual(self.antennafield_proxy.state(), DevState.OFF) @@ -282,9 +284,9 @@ class TestPowerHierarchyDevice(base.IntegrationTestCase): self.stationmanager_proxy.station_standby() # Reverse to HIBERNATE self.stationmanager_proxy.station_hibernate() + self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE") self.assertEqual( - self.stationmanager_proxy.last_requested_transition_R, - "STANDBY -> HIBERNATE", + self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE" ) self.assertEqual(self.apspu_h0_proxy.state(), DevState.OFF) self.assertEqual(self.apspu_l0_proxy.state(), DevState.OFF) diff --git a/tangostationcontrol/tangostationcontrol/common/states.py b/tangostationcontrol/tangostationcontrol/common/states.py index e5664f35234cbe659f2c86d5eb36ba8618e0fb47..f106dc56c62e9a8cb917047f633e1e7c9b5dbec7 100644 --- a/tangostationcontrol/tangostationcontrol/common/states.py +++ b/tangostationcontrol/tangostationcontrol/common/states.py @@ -1,7 +1,7 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from enum import Enum +from enum import IntEnum from typing import Dict, Optional from functools import wraps @@ -36,13 +36,13 @@ POWER_OFF_COMMAND_STATES = OPERATIONAL_STATES + [DevState.STANDBY, DevState.DISA # ----------------------- -class StationState(Enum): +class StationState(IntEnum): """Station states enumeration""" - OFF = "OFF" - HIBERNATE = "HIBERNATE" - STANDBY = "STANDBY" - ON = "ON" + OFF = 0 + HIBERNATE = 1 + STANDBY = 2 + ON = 3 # Contains which transitions are allowed for a given states @@ -58,6 +58,7 @@ DEVICES_ON_IN_STATION_STATE: Dict[str, Optional[StationState]] = { """In which StationState each device class should be switched ON.""" "StationManager": StationState.HIBERNATE, "CCD": StationState.HIBERNATE, + "EC": StationState.HIBERNATE, "PCON": StationState.HIBERNATE, "PSOC": StationState.HIBERNATE, "TemperatureManager": StationState.HIBERNATE, diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py index efebe69bba22712b1d95ac23a400eba6f435ef57..8de3c9f59f47071943de252d333b5b046ceaac50 100644 --- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py +++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py @@ -219,7 +219,7 @@ class PowerHierarchyDevice(AbstractHierarchyDevice): @suppress_exceptions(self.continue_on_failure) def power_antennas_on(device: DeviceProxy): # AntennaField: Power on used antennas - if device_class_matches(device, "AntennaField"): + if device_class_matches(device, ("AFL", "AFH")): logger.info("Powering on %s: Antennas", device) device.power_hardware_on() # TODO(JDM): Report which antennas @@ -252,7 +252,7 @@ class PowerHierarchyDevice(AbstractHierarchyDevice): @run_if_device_on_in_station_state(StationState.ON) def power_off_from_on(device: DeviceProxy): # AntennaField: Power off all antennas - if device_class_matches(device, "AntennaField"): + if device_class_matches(device, ("AFL", "AFH")): logger.info("Powering off %s: Antennas", device) device.power_hardware_off() # TODO(JDM): Report which antennas diff --git a/tangostationcontrol/tangostationcontrol/devices/calibration.py b/tangostationcontrol/tangostationcontrol/devices/calibration.py index ff3cb3280b754e624eaf7aa46bb5dee3bfc8c4bf..4d2a66381d31ce8c4782672b7afea012f27022d3 100644 --- a/tangostationcontrol/tangostationcontrol/devices/calibration.py +++ b/tangostationcontrol/tangostationcontrol/devices/calibration.py @@ -4,10 +4,9 @@ """ Calibration Device Server for LOFAR2.0 """ -import datetime import logging -import numpy +from prometheus_client import Counter from tango import EventType, Database from tango.server import device_property, command, attribute from tangostationcontrol.common.calibration import ( @@ -32,7 +31,7 @@ from tangostationcontrol.devices.antennafield.afl import AFL from tangostationcontrol.devices.base_device_classes.lofar_device import LOFARDevice from tangostationcontrol.devices.sdp.firmware import SDPFirmware from tangostationcontrol.devices.sdp.sdp import SDP -from tangostationcontrol.metrics import device_metrics +from tangostationcontrol.metrics import device_metrics, AttributeMetric, device_labels logger = logging.getLogger() __all__ = ["Calibration"] @@ -51,13 +50,18 @@ class Calibration(LOFARDevice): self.hba_proxies: CaseInsensitiveDict = CaseInsensitiveDict() self.lba_proxies: CaseInsensitiveDict = CaseInsensitiveDict() self.ant_proxies: CaseInsensitiveDict = CaseInsensitiveDict() - self.last_ant_calibration_timestamp: CaseInsensitiveDict[ - str, datetime.datetime | None - ] = CaseInsensitiveDict() # Super must be called after variable assignment due to executing init_device! super().__init__(cl, name) + self.calibration_count_metric = AttributeMetric( + "calibration_count", + "Number of times calibration has been triggered for each AntennaField device", + device_labels(self), + Counter, + dynamic_labels=["antennafield"], + ) + def _calibrate_antenna_field(self, device): """Recalibrate a specific AntennaField.""" @@ -69,11 +73,14 @@ class Calibration(LOFARDevice): logger.info("Re-calibrate antenna field %s", device) - self.last_ant_calibration_timestamp[device] = datetime.datetime.now() - self.calibrate_recv(device) self.calibrate_sdp(device) + # get device member in its original casing + antenna_field_name = device.get_name().split("/")[2] + + self.calibration_count_metric.get_metric([antenna_field_name]).inc() + @log_exceptions() def _antennafield_changed_event(self, event): """Trigger on key external changes in AntennaField settings.""" @@ -119,10 +126,14 @@ class Calibration(LOFARDevice): for k, ant in self.ant_proxies.items(): # Recalibrate associated AntennaFields sdpfirmware_device = ant.SDPFirmware_device_R - sdp_device = self.sdpfirmware_proxies[sdpfirmware_device].SDP_device_R - if device_name_matches(sdp_device, event.device.name()): + if device_name_matches(sdpfirmware_device, event.device.name()): self._calibrate_antenna_field(k) + break + else: + logger.warning( + f"Could not find any AntennaField to calibrate for clock change event from {event.device}" + ) # TODO(JDM): While we could read this from our control parent (StationManager), # doing so creates a deadlock when StationManager wants to initialise this @@ -147,16 +158,6 @@ class Calibration(LOFARDevice): def AntennaFields_Monitored_R(self): return list(self.ant_proxies.keys()) - @attribute(dtype=(numpy.int64,), max_dim_x=20) - def Last_AntennaField_Calibration_Timestamp_R(self): - return numpy.array( - [ - ts.timestamp() if ts else 0 - for ts in self.last_ant_calibration_timestamp.values() - ], - dtype=numpy.int64, - ) - @attribute(dtype=(str,), max_dim_x=20) def SDPs_Monitored_R(self): return list(self.sdp_proxies.keys()) @@ -256,7 +257,6 @@ class Calibration(LOFARDevice): } for d in devices: logger.debug("found HBA antenna field device %s", str(d)) - self.last_ant_calibration_timestamp[d] = None devices = db.get_device_exported_for_class(AFL.__name__) self.lba_proxies = { diff --git a/tangostationcontrol/tangostationcontrol/devices/observation_field.py b/tangostationcontrol/tangostationcontrol/devices/observation_field.py index 5a1452033dce20fdefcb7a8f2a9b6d183d9598e3..df970322aa075ff9522aa8d8fccac13217ea6ba5 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation_field.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation_field.py @@ -15,6 +15,7 @@ from jsonschema.exceptions import ValidationError from tango import AttrWriteType, DeviceProxy, DevState, Util from tango.server import attribute from tangostationcontrol.common.constants import ( + DEFAULT_METRICS_POLLING_PERIOD, DEFAULT_POLLING_PERIOD, MAX_ANTENNA, N_beamlets_ctrl, @@ -220,9 +221,9 @@ class ObservationField(LOFARDevice): def HBA_tile_beam_R(self): try: if self._observation_field_settings.HBA.tile_beam is None: - return None + return [] except AttributeError: - return None + return [] pointing_direction = self._observation_field_settings.HBA.tile_beam return [ @@ -308,6 +309,10 @@ class ObservationField(LOFARDevice): self._observation_field_settings.antenna_field, ) + # TODO(JDM): Somehow this does not get configured automatically + # as it does for non-dynamic devices. + self.poll_command("poll_attributes", DEFAULT_METRICS_POLLING_PERIOD) + def configure_for_off(self): """Indicate the observation has stopped""" diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py index 1bcf4581d08e8f95d5f66a1139700fd36cc2c7be..9efa55076757c6043e0446b74e6d12c0609ef075 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py @@ -54,11 +54,11 @@ class SDP(OPCUADevice): # Device Properties # ----------------- - # By default, do not enable processing until: + # By default, do not enable processing when: # * the ring is configured by this device # * the (number of) beamlet output destinations is configured by the beamlet device FPGA_processing_enable_RW_default = device_property( - dtype="DevVarBooleanArray", mandatory=False, default_value=[False] * N_pn + dtype="DevVarBooleanArray", mandatory=False, default_value=[True] * N_pn ) FPGA_ring_node_offset_RW_default = device_property( diff --git a/tangostationcontrol/tangostationcontrol/devices/station_manager.py b/tangostationcontrol/tangostationcontrol/devices/station_manager.py index f7963354cab92f26f61e235134073003e1b80a6d..dbe9f6052d82e28e0925af0f73bf7a284f557faf 100644 --- a/tangostationcontrol/tangostationcontrol/devices/station_manager.py +++ b/tangostationcontrol/tangostationcontrol/devices/station_manager.py @@ -72,13 +72,17 @@ class StationManager(AsyncDevice): def station_name_R(self): return self.Station_Name - @attribute(dtype=str, fisallowed="is_attribute_access_allowed") + @attribute(dtype=StationState, fisallowed="is_attribute_access_allowed") def station_state_R(self): - return self.station_state.name + return self.station_state - @attribute(dtype=str, fisallowed="is_attribute_access_allowed") - def last_requested_transition_R(self): - return self.last_requested_transition or "" + @attribute(dtype=StationState, fisallowed="is_attribute_access_allowed") + def requested_station_state_R(self): + return self.requested_station_state + + @attribute(dtype=bool, fisallowed="is_attribute_access_allowed") + def station_state_transitioning_R(self): + return self.transition_lock and self.transition_lock.locked() @attribute(dtype=(str,), max_dim_x=1024, fisallowed="is_attribute_access_allowed") def last_requested_transition_exceptions_R(self): @@ -98,7 +102,7 @@ class StationManager(AsyncDevice): def __init__(self, cl, name): self.station_state = StationState.OFF self.stationmanager_ph = None - self.last_requested_transition = None + self.requested_station_state = StationState.OFF self.last_requested_transition_exceptions = [] self.transition_lock = asyncio.Lock() @@ -171,7 +175,7 @@ class StationManager(AsyncDevice): async def _transition( self, - transition_desc: str, + target_state: StationState, transition_func: Callable[[], Awaitable[None]], ): """Transition to a station state using `transition_func`. @@ -187,13 +191,14 @@ class StationManager(AsyncDevice): ) logger.info( - "Station %s requested to perform the %s Power Sequence", + "Station %s requested to perform the %s -> %s Power Sequence", self.Station_Name, - transition_desc, + self.station_state.name, + target_state.name, ) try: - self.last_requested_transition = transition_desc + self.requested_station_state = target_state self.last_requested_transition_exceptions = await transition_func() except Exception as ex: # unsuppressed exception @@ -201,9 +206,10 @@ class StationManager(AsyncDevice): raise logger.info( - "Station %s has correctly completed the %s Power Sequence", + "Station %s has correctly completed the %s -> %s Power Sequence", self.Station_Name, - transition_desc, + self.station_state.name, + target_state.name, ) # -------- @@ -226,6 +232,7 @@ class StationManager(AsyncDevice): # not implemented -> call the correct state transition function # update the station_state variable when successful + self.requested_station_state = StationState.OFF self.station_state = StationState.OFF finally: self.transition_lock.release() @@ -247,11 +254,11 @@ class StationManager(AsyncDevice): try: if self.station_state == StationState.OFF: await self._transition( - "OFF -> HIBERNATE", self.stationmanager_ph.off_to_hibernate + StationState.HIBERNATE, self.stationmanager_ph.off_to_hibernate ) elif self.station_state == StationState.STANDBY: await self._transition( - "STANDBY -> HIBERNATE", + StationState.HIBERNATE, self.stationmanager_ph.standby_to_hibernate, ) except DevFailed as exc: @@ -283,12 +290,12 @@ class StationManager(AsyncDevice): try: if self.station_state == StationState.HIBERNATE: await self._transition( - "HIBERNATE -> STANDBY", + StationState.STANDBY, self.stationmanager_ph.hibernate_to_standby, ) elif self.station_state == StationState.ON: await self._transition( - "ON -> STANDBY", self.stationmanager_ph.on_to_standby + StationState.STANDBY, self.stationmanager_ph.on_to_standby ) except DevFailed as exc: error_string = f"Station {self.Station_Name} \ @@ -318,7 +325,7 @@ class StationManager(AsyncDevice): # call the correct state transition function try: await self._transition( - "STANDBY -> ON", self.stationmanager_ph.standby_to_on + StationState.ON, self.stationmanager_ph.standby_to_on ) except DevFailed as exc: error_string = f"Station {self.Station_Name} \ diff --git a/tangostationcontrol/tangostationcontrol/metrics/_decorators.py b/tangostationcontrol/tangostationcontrol/metrics/_decorators.py index 23bdce5a9f2605d3aea327351f91f36d4a9780d6..65ddd4dc2b52525356a1e9965e0639df312425c2 100644 --- a/tangostationcontrol/tangostationcontrol/metrics/_decorators.py +++ b/tangostationcontrol/tangostationcontrol/metrics/_decorators.py @@ -6,7 +6,7 @@ from typing import List from tango import Attribute, DevState from tango.server import Device, attribute -from prometheus_client import Enum, Metric +from prometheus_client import Enum from prometheus_client.core import Info, Counter from tangostationcontrol import __version__ as version @@ -38,22 +38,17 @@ class VersionMetric(AttributeMetric): class StateMetric(AttributeMetric): def __init__(self, device: Device): - super().__init__("state", "State of the device.", device.metric_labels, Enum) - - self.set_state(device.get_state()) - - wrap_method(device, device.set_state, self.set_state, post_execute=False) - - def make_metric(self) -> Metric: - return Enum( - self.name, - self.description, - labelnames=self.label_keys(), - states=list(DevState.names), + super().__init__( + "state", + "State of the device.", + device.metric_labels, + Enum, + metric_class_init_kwargs={"states": list(DevState.names)}, ) - def set_state(self, state): - self.get_metric().state(state.name) + self.set_value(device.get_state()) + + wrap_method(device, device.set_state, self.set_value, post_execute=False) class AccessCountMetric(AttributeMetric): diff --git a/tangostationcontrol/tangostationcontrol/metrics/_metrics.py b/tangostationcontrol/tangostationcontrol/metrics/_metrics.py index 715ce81f0b9e918d22a670b020b3b89081a70fc9..a8a5490cbf43d855ff678a6486256300a269def3 100644 --- a/tangostationcontrol/tangostationcontrol/metrics/_metrics.py +++ b/tangostationcontrol/tangostationcontrol/metrics/_metrics.py @@ -3,8 +3,10 @@ from tango import AttrWriteType from tango import CmdArgType from tango import Attribute from tango import DevFailed -from prometheus_client import Metric, Gauge, Info +from tango import DevState +from prometheus_client import Metric, Gauge, Info, Enum from asyncio import iscoroutinefunction +from enum import IntEnum from typing import List, Dict, Callable, Union import functools import logging @@ -112,6 +114,8 @@ class AttributeMetric: description: str, static_labels: Dict[str, str], metric_class=Gauge, + metric_class_init_kwargs: Dict[str, object] | None = None, + dynamic_labels: List[str] | None = None, ): self.name = metric_name(name) self.description = description @@ -120,6 +124,10 @@ class AttributeMetric: self.static_label_keys = list(static_labels.keys()) self.static_label_values = list(static_labels.values()) + self.dynamic_label_keys = dynamic_labels or [] + + self.metric_class_init_kwargs = metric_class_init_kwargs or {} + if self.name not in METRICS: METRICS[self.name] = self.make_metric() @@ -131,23 +139,31 @@ class AttributeMetric: def label_keys(self) -> List[str]: """Return the list of labels that we will use.""" - return self.static_label_keys + return self.static_label_keys + self.dynamic_label_keys def make_metric(self) -> Metric: """Construct a metric that collects samples for this attribute.""" return self.metric_class( - self.name, self.description, labelnames=self.label_keys() + self.name, + self.description, + labelnames=self.label_keys(), + **self.metric_class_init_kwargs, ) - def get_metric(self, extra_labels: List = None) -> Metric: + def get_metric(self, dynamic_label_values: List = None) -> Metric: """Return the metric that uses the default labels.""" - return self.metric.labels(*self.static_label_values, *(extra_labels or [])) + return self.metric.labels( + *self.static_label_values, *(dynamic_label_values or []) + ) def set_value(self, value: object): """A new value for the attribute is known. Feed it to the metric.""" # set it, this class will take care of the default labels - self._set_value(value, self.static_label_values) + if self.metric_class == Enum: + self._enum_value(value, self.static_label_values) + else: + self._set_value(value, self.static_label_values) def _set_value(self, value: object, labels: List[str]): assert self.metric_class == Gauge @@ -157,6 +173,12 @@ class AttributeMetric: assert self.metric_class == Info self.metric.labels(*labels).info(value) + def _enum_value(self, value: str | IntEnum, labels: List[str]): + assert self.metric_class == Enum + self.metric.labels(*labels).state( + value.name if isinstance(value, (DevState, IntEnum)) else value + ) + def collect(self) -> List[Metric]: """Return all collected samples.""" return self.metric.collect() @@ -183,6 +205,19 @@ class ScalarAttributeMetric(AttributeMetric): if self.data_type == CmdArgType.DevString: super().__init__(attribute.get_name(), description, static_labels, Info) + elif self.data_type == CmdArgType.DevEnum: + # evil PyTango foo to obtain enum labels from class attribute + enum_labels = getattr( + device.__class__, attribute.get_name() + ).att_prop.enum_labels.split(",") + + super().__init__( + attribute.get_name(), + description, + static_labels, + Enum, + metric_class_init_kwargs={"states": enum_labels}, + ) else: super().__init__(attribute.get_name(), description, static_labels) diff --git a/tangostationcontrol/test/metrics/test_metrics.py b/tangostationcontrol/test/metrics/test_metrics.py index 3d084a9c24b75c01f9de2644f108f79f199448a2..351d5f43f2c525643befaff1739d1fb66974c081 100644 --- a/tangostationcontrol/test/metrics/test_metrics.py +++ b/tangostationcontrol/test/metrics/test_metrics.py @@ -10,6 +10,7 @@ from tango.server import ( from tango.test_context import DeviceTestContext from prometheus_client import generate_latest from prometheus_client.registry import REGISTRY +from enum import IntEnum from typing import Dict import asyncio import numpy @@ -107,6 +108,10 @@ class TestMetrics(base.TestCase): def test_scalar_attribute_metric(self): """Test ScalarAttributeMetric""" + class MyEnum(IntEnum): + ZERO = 0 + ONE = 1 + class test_device(Device): float_attr = attribute( doc="docstr", @@ -120,10 +125,17 @@ class TestMetrics(base.TestCase): fget=lambda obj: "foo", ) + enum_attr = attribute( + doc="docstr", + dtype=MyEnum, + fget=lambda obj: MyEnum.ONE, + ) + def init_device(self): # create an attribute metric and assign a value self.float_metric = ScalarAttributeMetric(self, self.float_attr) self.str_metric = ScalarAttributeMetric(self, self.str_attr) + self.enum_metric = ScalarAttributeMetric(self, self.enum_attr) @command() def test(device): @@ -166,10 +178,44 @@ class TestMetrics(base.TestCase): metric.samples[0].labels, ) + # check collected metrics (enum_attr) + metric = device.enum_metric.metric.collect()[0] + + self.assertEqual("ds_enum_attr", metric.name) + self.assertEqual("docstr", metric.documentation) + + # check labels as the DeviceTestContext would result in + self.assertDictEqual( + { + "domain": "test", + "family": "nodb", + "member": "test_device", + "device_class": "test_device", + "access": "r", + "ds_enum_attr": "ZERO", + }, + metric.samples[0].labels, + ) + self.assertEqual(0, metric.samples[0].value) + + self.assertDictEqual( + { + "domain": "test", + "family": "nodb", + "member": "test_device", + "device_class": "test_device", + "access": "r", + "ds_enum_attr": "ONE", + }, + metric.samples[1].labels, + ) + self.assertEqual(1, metric.samples[1].value) + with DeviceTestContext(test_device, process=False) as proxy: # access the attribute to trigger value propagation to metric _ = proxy.float_attr _ = proxy.str_attr + _ = proxy.enum_attr proxy.test()