diff --git a/README.md b/README.md index 7c04992fb1366331722dfac44ee1452b0ebd38be..7bc350f5a1d684e7b163cd4b59b76bf587b4b28e 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ Next change the version in the following places: # Release Notes +* 0.42.5 Add additional features to protection control * 0.42.4 Add integration test fixture that routinely tests against cross test dependencies * 0.42.3 Use PyTango 10.0.0rc3 to reduce memory leaks * 0.42.2 Add protection control device shutting down station during over temperature diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION index ff1a48ef977a26addbf512cc34c09ff6ccc80d65..56754720a518bb276eef78834b2bb8f7bf9cd28c 100644 --- a/tangostationcontrol/VERSION +++ b/tangostationcontrol/VERSION @@ -1 +1 @@ -0.42.4 +0.42.5 diff --git a/tangostationcontrol/integration_test/default/devices/test_device_protection_control.py b/tangostationcontrol/integration_test/default/devices/test_device_protection_control.py index 794ce8816c253b811dd6b16277eb7be7f1a50704..e539aeadc1ed5ff68625420e1d0ea1968f344121 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_protection_control.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_protection_control.py @@ -8,7 +8,7 @@ from typing import List import timeout_decorator from tango import Database, DevState -from tangostationcontrol.devices import UNB2, SDPFirmware, RECVL, RECVH, APSCT +from tangostationcontrol.devices import UNB2, SDPFirmware, RECVL, RECVH, APSCT, APSPU from integration_test.default.devices.base import TestDeviceBase @@ -30,6 +30,7 @@ class TestDeviceProtectionControl(TestDeviceBase): recvl_devices = db.get_device_exported_for_class(RECVL.__name__) recvh_devices = db.get_device_exported_for_class(RECVH.__name__) apsct_devices = db.get_device_exported_for_class(APSCT.__name__) + apspu_devices = db.get_device_exported_for_class(APSPU.__name__) self.setup_proxies(unb2_devices) self.setup_proxies(sdpfirmware_devices) @@ -37,6 +38,7 @@ class TestDeviceProtectionControl(TestDeviceBase): self.setup_proxies(recvh_devices) self.setup_proxies(unb2_devices) self.setup_proxies(apsct_devices) + self.setup_proxies(apspu_devices) super().setUp("stat/protectioncontrol/1") diff --git a/tangostationcontrol/tangostationcontrol/devices/protection_control.py b/tangostationcontrol/tangostationcontrol/devices/protection_control.py index 0ef1ab24cf29e173b37789d17b641c945c683209..dba1916a6d6d8aab0e716cbc4fd845849f4fa786 100644 --- a/tangostationcontrol/tangostationcontrol/devices/protection_control.py +++ b/tangostationcontrol/tangostationcontrol/devices/protection_control.py @@ -43,7 +43,6 @@ from tangostationcontrol.protection.metrics import ProtectionMetrics from tangostationcontrol.protection.protection_manager import ProtectionManager from tangostationcontrol.protection.state import ProtectionStateEnum from tangostationcontrol.protection.threshold import ( - NumberProtectionThreshold, FilteredNumberProtectionThreshold, ) @@ -62,26 +61,48 @@ class ProtectionControl(LOFARDevice): { "UNB2": CaseInsensitiveDict( { - "UNB2_FPGA_POL_CORE_TEMP_R": NumberProtectionThreshold( - minimal=0.0, maximum=90.0 + "UNB2_FPGA_POL_CORE_TEMP_R": FilteredNumberProtectionThreshold( + minimal=0.0, maximum=90.0, lower_limit=-128.0, upper_limit=350.0 ) } ), "SDPFirmware": CaseInsensitiveDict( { "FPGA_temp_R": FilteredNumberProtectionThreshold( - minimal=0.0, maximum=90.0, lower_limit=-273.0, upper_limit=350.0 + minimal=0.0, maximum=90.0, lower_limit=-128.0, upper_limit=350.0 ) } ), "RECVL": CaseInsensitiveDict( - {"RCU_TEMP_R": NumberProtectionThreshold(minimal=0.0, maximum=90.0)} + { + "RCU_TEMP_R": FilteredNumberProtectionThreshold( + minimal=0.0, maximum=90.0, lower_limit=-128.0, upper_limit=350.0 + ) + } ), "RECVH": CaseInsensitiveDict( - {"RCU_TEMP_R": NumberProtectionThreshold(minimal=0.0, maximum=90.0)} + { + "RCU_TEMP_R": FilteredNumberProtectionThreshold( + minimal=0.0, maximum=90.0, lower_limit=-128.0, upper_limit=350.0 + ) + } ), "APSCT": CaseInsensitiveDict( - {"APSCT_TEMP_R": NumberProtectionThreshold(minimal=0.0, maximum=90.0)} + { + "APSCT_TEMP_R": FilteredNumberProtectionThreshold( + minimal=0.0, maximum=90.0, lower_limit=-128.0, upper_limit=350.0 + ) + } + ), + "APSPU": CaseInsensitiveDict( + { + "APSPU_RCU2A_TEMP_R": FilteredNumberProtectionThreshold( + minimal=0.0, + maximum=100.0, + lower_limit=-128.0, + upper_limit=350.0, + ) + } ), } ) @@ -109,6 +130,8 @@ class ProtectionControl(LOFARDevice): def state_R(self): if self._protection_manager: return self._protection_manager.state + else: + return ProtectionStateEnum.DEACTIVATED @attribute( doc="Whether the station is periodically evaluated against damage", @@ -166,6 +189,14 @@ class ProtectionControl(LOFARDevice): pairs.append(f"{device_name}.{attribute_name}") return pairs + @attribute( + doc="The last device attribute pair change event that was above the threshold," + "this value can be extremely stale!", + dtype=str, + ) + def last_threshold_device_attribute(self): + return self._metrics.threshold_device_attribute + # -------- # Overloaded functions # -------- @@ -227,9 +258,14 @@ class ProtectionControl(LOFARDevice): self, device: DeviceProxy, attribute_name: str, value: Any ): try: - self._protection_manager.evaluate( + if self._protection_manager.evaluate( device.info().dev_class, attribute_name, value - ) + ): + logger.error( + "Device: %s has attribute: %s that is above threshold", + device.name(), + attribute_name, + ) self._metrics.update_change_event(device.name(), attribute_name) except Exception: logger.exception( diff --git a/tangostationcontrol/tangostationcontrol/protection/metrics.py b/tangostationcontrol/tangostationcontrol/protection/metrics.py index a475ef985584e8ef86783e754f358838f5508329..ae24403126297913bb410d97bbdd051447e3978a 100644 --- a/tangostationcontrol/tangostationcontrol/protection/metrics.py +++ b/tangostationcontrol/tangostationcontrol/protection/metrics.py @@ -20,6 +20,7 @@ class ProtectionMetrics: """Collecting and serving metrics about protecting station against damage""" def __init__(self, mapping: protection_metric_mapping_type): + self._threshold_device_attribute = "" self._number_of_discovered_devices = 0 self._number_of_connected_devices = 0 self._device_names: List[str] = [] @@ -47,6 +48,10 @@ class ProtectionMetrics: def time_last_change_events(self) -> List[int]: return self._time_last_change_events + @property + def threshold_device_attribute(self) -> str: + return self._threshold_device_attribute + def update_device_metrics(self, proxies: device_proxy_type): """Update statistics about device connection metrics""" @@ -70,3 +75,6 @@ class ProtectionMetrics: device_name, attribute_name, ) + + def update_threshold_device_attribute(self, device_name: str, attribute_name: str): + self._threshold_device_attribute = f"{device_name}.{attribute_name}" diff --git a/tangostationcontrol/tangostationcontrol/protection/protection_manager.py b/tangostationcontrol/tangostationcontrol/protection/protection_manager.py index de75b1bd23ae9a5f7303195c42811b6d38fae6ea..3867a976d00996102be63c6470055c0ca6332649 100644 --- a/tangostationcontrol/tangostationcontrol/protection/protection_manager.py +++ b/tangostationcontrol/tangostationcontrol/protection/protection_manager.py @@ -62,12 +62,13 @@ class ProtectionManager(ICreateProxies): convert_protection_to_device_config(self._config), self._proxies ) - def evaluate(self, device_class: str, attribute_name: str, value: Any): + def evaluate(self, device_class: str, attribute_name: str, value: Any) -> bool: """Evaluate if the attribute value should trigger a protective shutdown""" - if self._config[device_class][attribute_name].evaluate(value): - logger.warning("Device: %s has attribute: %s that is above threshold") + result = self._config[device_class][attribute_name].evaluate(value) + if result: self.protective_shutdown() + return result def _transition_minus_one(self, reference_state: StationStateEnum): try: diff --git a/tangostationcontrol/tangostationcontrol/protection/state.py b/tangostationcontrol/tangostationcontrol/protection/state.py index e43bc5e3b860d40bf1210f7c39bb6bcebc6e7737..34304443206cbb1826e311ade590cc8b1a1d9970 100644 --- a/tangostationcontrol/tangostationcontrol/protection/state.py +++ b/tangostationcontrol/tangostationcontrol/protection/state.py @@ -19,12 +19,13 @@ class ProtectionStateEnum(IntEnum): transitioning -> active """ - OFF = 0 # No protective shutdown is ongoing - ARMED = 1 # A protective shutdown is being prepared - ACTIVE = 2 # A protective shutdown is ongoing and the stationmanager is locked against state transitions - TRANSITIONING = 3 # Protective shutdown transitions are taking place, the shutdown is ongoing the stationmanager is locked - FAULT = 4 # The protective shutdown encountered an unrecoverable exception - ABORTED = 5 # If the protective shutdown was aborted before completion + DEACTIVATED = 0 # protective control measures are disabled outright + OFF = 1 # No protective shutdown is ongoing + ARMED = 2 # A protective shutdown is being prepared + ACTIVE = 3 # A protective shutdown is ongoing and the stationmanager is locked against state transitions + TRANSITIONING = 4 # Protective shutdown transitions are taking place, the shutdown is ongoing the stationmanager is locked + FAULT = 5 # The protective shutdown encountered an unrecoverable exception + ABORTED = 6 # If the protective shutdown was aborted before completion """""" diff --git a/tangostationcontrol/tangostationcontrol/protection/threshold.py b/tangostationcontrol/tangostationcontrol/protection/threshold.py index 52c77ea339ebec904d4b32b6ad5a79ffeeb9e7e1..4ffce522c42fc5b0fc091325e0519d8cafaa0f75 100644 --- a/tangostationcontrol/tangostationcontrol/protection/threshold.py +++ b/tangostationcontrol/tangostationcontrol/protection/threshold.py @@ -30,12 +30,12 @@ class NumberProtectionThreshold(IProtectionThreshold): :return: returns true if any value is above or below threshold """ if isinstance(value, numpy.ndarray): - return numpy.any(value > maximum) or numpy.any(value < minimal) + return numpy.any(value > maximum) # or numpy.any(value < minimal) elif sequence_not_str(value) and len(value) > 0: for element in value: if NumberProtectionThreshold._evaluate(element, minimal, maximum): return True - elif value > maximum or value < minimal: + elif value > maximum: # or value < minimal: return True return False diff --git a/tangostationcontrol/test/protection/test_threshold.py b/tangostationcontrol/test/protection/test_threshold.py index ac21f62b9e565659f44ba91c4e9f7bac57e1bb0c..7c958e0411aa5d2852ea6d4857d13f08d1c15e42 100644 --- a/tangostationcontrol/test/protection/test_threshold.py +++ b/tangostationcontrol/test/protection/test_threshold.py @@ -23,8 +23,8 @@ class TestProtectionThreshold(base.TestCase): test = NumberProtectionThreshold(minimal=0, maximum=90.0) - self.assertTrue(test.evaluate(-1)) - self.assertTrue(test.evaluate(-0.1)) + # self.assertTrue(test.evaluate(-1)) + # self.assertTrue(test.evaluate(-0.1)) self.assertTrue(test.evaluate(91)) self.assertTrue(test.evaluate(90.1)) @@ -35,10 +35,10 @@ class TestProtectionThreshold(base.TestCase): def test_number_threshold_list(self): test = NumberProtectionThreshold(minimal=0, maximum=90.0) - self.assertTrue( - test.evaluate([0, 0, 0, 0, -1]), - "Single dimensionsal list below threshold failed", - ) + # self.assertTrue( + # test.evaluate([0, 0, 0, 0, -1]), + # "Single dimensionsal list below threshold failed", + # ) self.assertTrue( test.evaluate( [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 91, 0, 0], [0, 0, 0, 0, 0]] @@ -60,18 +60,18 @@ class TestProtectionThreshold(base.TestCase): def test_number_threshold_tuple(self): test = NumberProtectionThreshold(minimal=0, maximum=90.0) - self.assertTrue( - test.evaluate( - ( - 0, - 0, - 0, - 0, - -1, - ) - ), - "Single dimensionsal tuple below threshold failed", - ) + # self.assertTrue( + # test.evaluate( + # ( + # 0, + # 0, + # 0, + # 0, + # -1, + # ) + # ), + # "Single dimensionsal tuple below threshold failed", + # ) self.assertTrue( test.evaluate( ( @@ -191,9 +191,9 @@ class TestProtectionThreshold(base.TestCase): minimal=StationStateEnum.HIBERNATE, maximum=StationStateEnum.STANDBY ) - self.assertTrue( - test.evaluate(StationStateEnum.OFF), "Enum scalar below threshold failed" - ) + # self.assertTrue( + # test.evaluate(StationStateEnum.OFF), "Enum scalar below threshold failed" + # ) self.assertTrue( test.evaluate(StationStateEnum.ON), "Enum scalar above threshold failed" @@ -204,14 +204,14 @@ class TestProtectionThreshold(base.TestCase): "Enum scalar within threshold failed", ) - self.assertTrue( - test.evaluate([StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]), - "Single dimensionsal list below threshold failed", - ) + # self.assertTrue( + # test.evaluate([StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]), + # "Single dimensionsal list below threshold failed", + # ) - self.assertTrue( - test.evaluate( - [[StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]] * 4 - ), - "Multidimension dimensionsal list below threshold failed", - ) + # self.assertTrue( + # test.evaluate( + # [[StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]] * 4 + # ), + # "Multidimension dimensionsal list below threshold failed", + # ) diff --git a/tangostationcontrol/tox.ini b/tangostationcontrol/tox.ini index 5b4789f4e76cbb54c4ab601dbbc1cba4eb34bcbb..0d61946397570350f94f9028a69a9b03fb88705b 100644 --- a/tangostationcontrol/tox.ini +++ b/tangostationcontrol/tox.ini @@ -121,4 +121,4 @@ commands = [flake8] filename = *.py,.stestr.conf,.txt ignore = B014, B019, B028, W291, W293, W391, E111, E114, E121, E122, E123, E124, E126, E127, E128, E131, E201, E201, E202, E203, E221, E222, E225, E226, E231, E241, E251, E252, E261, E262, E265, E271, E301, E302, E303, E305, E306, E401, E402, E501, E502, E701, E712, E721, E731, F403, F523, F541, F841, H301, H306, H401, H403, H404, H405, W503 -exclude = SNMP_mib_loading,output_pymibs +exclude = SNMP_mib_loading,output_pymibs,_proto