Skip to content
Snippets Groups Projects
Commit 2f4e64ab authored by Corné Lukken's avatar Corné Lukken
Browse files

Merge branch 'L2SS-1986' into 'master'

L2SS-1986: Process new requirements and fix bugs

Closes L2SS-1986

See merge request !980
parents 24d6eccc 36c5888f
Branches
Tags
1 merge request!980L2SS-1986: Process new requirements and fix bugs
...@@ -151,6 +151,7 @@ Next change the version in the following places: ...@@ -151,6 +151,7 @@ Next change the version in the following places:
# Release Notes # 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.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.3 Use PyTango 10.0.0rc3 to reduce memory leaks
* 0.42.2 Add protection control device shutting down station during over temperature * 0.42.2 Add protection control device shutting down station during over temperature
......
0.42.4 0.42.5
...@@ -8,7 +8,7 @@ from typing import List ...@@ -8,7 +8,7 @@ from typing import List
import timeout_decorator import timeout_decorator
from tango import Database, DevState 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 from integration_test.default.devices.base import TestDeviceBase
...@@ -30,6 +30,7 @@ class TestDeviceProtectionControl(TestDeviceBase): ...@@ -30,6 +30,7 @@ class TestDeviceProtectionControl(TestDeviceBase):
recvl_devices = db.get_device_exported_for_class(RECVL.__name__) recvl_devices = db.get_device_exported_for_class(RECVL.__name__)
recvh_devices = db.get_device_exported_for_class(RECVH.__name__) recvh_devices = db.get_device_exported_for_class(RECVH.__name__)
apsct_devices = db.get_device_exported_for_class(APSCT.__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(unb2_devices)
self.setup_proxies(sdpfirmware_devices) self.setup_proxies(sdpfirmware_devices)
...@@ -37,6 +38,7 @@ class TestDeviceProtectionControl(TestDeviceBase): ...@@ -37,6 +38,7 @@ class TestDeviceProtectionControl(TestDeviceBase):
self.setup_proxies(recvh_devices) self.setup_proxies(recvh_devices)
self.setup_proxies(unb2_devices) self.setup_proxies(unb2_devices)
self.setup_proxies(apsct_devices) self.setup_proxies(apsct_devices)
self.setup_proxies(apspu_devices)
super().setUp("stat/protectioncontrol/1") super().setUp("stat/protectioncontrol/1")
......
...@@ -43,7 +43,6 @@ from tangostationcontrol.protection.metrics import ProtectionMetrics ...@@ -43,7 +43,6 @@ from tangostationcontrol.protection.metrics import ProtectionMetrics
from tangostationcontrol.protection.protection_manager import ProtectionManager from tangostationcontrol.protection.protection_manager import ProtectionManager
from tangostationcontrol.protection.state import ProtectionStateEnum from tangostationcontrol.protection.state import ProtectionStateEnum
from tangostationcontrol.protection.threshold import ( from tangostationcontrol.protection.threshold import (
NumberProtectionThreshold,
FilteredNumberProtectionThreshold, FilteredNumberProtectionThreshold,
) )
...@@ -62,26 +61,48 @@ class ProtectionControl(LOFARDevice): ...@@ -62,26 +61,48 @@ class ProtectionControl(LOFARDevice):
{ {
"UNB2": CaseInsensitiveDict( "UNB2": CaseInsensitiveDict(
{ {
"UNB2_FPGA_POL_CORE_TEMP_R": NumberProtectionThreshold( "UNB2_FPGA_POL_CORE_TEMP_R": FilteredNumberProtectionThreshold(
minimal=0.0, maximum=90.0 minimal=0.0, maximum=90.0, lower_limit=-128.0, upper_limit=350.0
) )
} }
), ),
"SDPFirmware": CaseInsensitiveDict( "SDPFirmware": CaseInsensitiveDict(
{ {
"FPGA_temp_R": FilteredNumberProtectionThreshold( "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( "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( "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": 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): ...@@ -109,6 +130,8 @@ class ProtectionControl(LOFARDevice):
def state_R(self): def state_R(self):
if self._protection_manager: if self._protection_manager:
return self._protection_manager.state return self._protection_manager.state
else:
return ProtectionStateEnum.DEACTIVATED
@attribute( @attribute(
doc="Whether the station is periodically evaluated against damage", doc="Whether the station is periodically evaluated against damage",
...@@ -166,6 +189,14 @@ class ProtectionControl(LOFARDevice): ...@@ -166,6 +189,14 @@ class ProtectionControl(LOFARDevice):
pairs.append(f"{device_name}.{attribute_name}") pairs.append(f"{device_name}.{attribute_name}")
return pairs 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 # Overloaded functions
# -------- # --------
...@@ -227,8 +258,13 @@ class ProtectionControl(LOFARDevice): ...@@ -227,8 +258,13 @@ class ProtectionControl(LOFARDevice):
self, device: DeviceProxy, attribute_name: str, value: Any self, device: DeviceProxy, attribute_name: str, value: Any
): ):
try: try:
self._protection_manager.evaluate( if self._protection_manager.evaluate(
device.info().dev_class, attribute_name, value 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) self._metrics.update_change_event(device.name(), attribute_name)
except Exception: except Exception:
......
...@@ -20,6 +20,7 @@ class ProtectionMetrics: ...@@ -20,6 +20,7 @@ class ProtectionMetrics:
"""Collecting and serving metrics about protecting station against damage""" """Collecting and serving metrics about protecting station against damage"""
def __init__(self, mapping: protection_metric_mapping_type): def __init__(self, mapping: protection_metric_mapping_type):
self._threshold_device_attribute = ""
self._number_of_discovered_devices = 0 self._number_of_discovered_devices = 0
self._number_of_connected_devices = 0 self._number_of_connected_devices = 0
self._device_names: List[str] = [] self._device_names: List[str] = []
...@@ -47,6 +48,10 @@ class ProtectionMetrics: ...@@ -47,6 +48,10 @@ class ProtectionMetrics:
def time_last_change_events(self) -> List[int]: def time_last_change_events(self) -> List[int]:
return self._time_last_change_events 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): def update_device_metrics(self, proxies: device_proxy_type):
"""Update statistics about device connection metrics""" """Update statistics about device connection metrics"""
...@@ -70,3 +75,6 @@ class ProtectionMetrics: ...@@ -70,3 +75,6 @@ class ProtectionMetrics:
device_name, device_name,
attribute_name, attribute_name,
) )
def update_threshold_device_attribute(self, device_name: str, attribute_name: str):
self._threshold_device_attribute = f"{device_name}.{attribute_name}"
...@@ -62,12 +62,13 @@ class ProtectionManager(ICreateProxies): ...@@ -62,12 +62,13 @@ class ProtectionManager(ICreateProxies):
convert_protection_to_device_config(self._config), self._proxies 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""" """Evaluate if the attribute value should trigger a protective shutdown"""
if self._config[device_class][attribute_name].evaluate(value): result = self._config[device_class][attribute_name].evaluate(value)
logger.warning("Device: %s has attribute: %s that is above threshold") if result:
self.protective_shutdown() self.protective_shutdown()
return result
def _transition_minus_one(self, reference_state: StationStateEnum): def _transition_minus_one(self, reference_state: StationStateEnum):
try: try:
......
...@@ -19,12 +19,13 @@ class ProtectionStateEnum(IntEnum): ...@@ -19,12 +19,13 @@ class ProtectionStateEnum(IntEnum):
transitioning -> active transitioning -> active
""" """
OFF = 0 # No protective shutdown is ongoing DEACTIVATED = 0 # protective control measures are disabled outright
ARMED = 1 # A protective shutdown is being prepared OFF = 1 # No protective shutdown is ongoing
ACTIVE = 2 # A protective shutdown is ongoing and the stationmanager is locked against state transitions ARMED = 2 # A protective shutdown is being prepared
TRANSITIONING = 3 # Protective shutdown transitions are taking place, the shutdown is ongoing the stationmanager is locked ACTIVE = 3 # A protective shutdown is ongoing and the stationmanager is locked against state transitions
FAULT = 4 # The protective shutdown encountered an unrecoverable exception TRANSITIONING = 4 # Protective shutdown transitions are taking place, the shutdown is ongoing the stationmanager is locked
ABORTED = 5 # If the protective shutdown was aborted before completion FAULT = 5 # The protective shutdown encountered an unrecoverable exception
ABORTED = 6 # If the protective shutdown was aborted before completion
"""""" """"""
......
...@@ -30,12 +30,12 @@ class NumberProtectionThreshold(IProtectionThreshold): ...@@ -30,12 +30,12 @@ class NumberProtectionThreshold(IProtectionThreshold):
:return: returns true if any value is above or below threshold :return: returns true if any value is above or below threshold
""" """
if isinstance(value, numpy.ndarray): 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: elif sequence_not_str(value) and len(value) > 0:
for element in value: for element in value:
if NumberProtectionThreshold._evaluate(element, minimal, maximum): if NumberProtectionThreshold._evaluate(element, minimal, maximum):
return True return True
elif value > maximum or value < minimal: elif value > maximum: # or value < minimal:
return True return True
return False return False
......
...@@ -23,8 +23,8 @@ class TestProtectionThreshold(base.TestCase): ...@@ -23,8 +23,8 @@ class TestProtectionThreshold(base.TestCase):
test = NumberProtectionThreshold(minimal=0, maximum=90.0) test = NumberProtectionThreshold(minimal=0, maximum=90.0)
self.assertTrue(test.evaluate(-1)) # self.assertTrue(test.evaluate(-1))
self.assertTrue(test.evaluate(-0.1)) # self.assertTrue(test.evaluate(-0.1))
self.assertTrue(test.evaluate(91)) self.assertTrue(test.evaluate(91))
self.assertTrue(test.evaluate(90.1)) self.assertTrue(test.evaluate(90.1))
...@@ -35,10 +35,10 @@ class TestProtectionThreshold(base.TestCase): ...@@ -35,10 +35,10 @@ class TestProtectionThreshold(base.TestCase):
def test_number_threshold_list(self): def test_number_threshold_list(self):
test = NumberProtectionThreshold(minimal=0, maximum=90.0) test = NumberProtectionThreshold(minimal=0, maximum=90.0)
self.assertTrue( # self.assertTrue(
test.evaluate([0, 0, 0, 0, -1]), # test.evaluate([0, 0, 0, 0, -1]),
"Single dimensionsal list below threshold failed", # "Single dimensionsal list below threshold failed",
) # )
self.assertTrue( self.assertTrue(
test.evaluate( test.evaluate(
[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 91, 0, 0], [0, 0, 0, 0, 0]] [[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): ...@@ -60,18 +60,18 @@ class TestProtectionThreshold(base.TestCase):
def test_number_threshold_tuple(self): def test_number_threshold_tuple(self):
test = NumberProtectionThreshold(minimal=0, maximum=90.0) test = NumberProtectionThreshold(minimal=0, maximum=90.0)
self.assertTrue( # self.assertTrue(
test.evaluate( # test.evaluate(
( # (
0, # 0,
0, # 0,
0, # 0,
0, # 0,
-1, # -1,
) # )
), # ),
"Single dimensionsal tuple below threshold failed", # "Single dimensionsal tuple below threshold failed",
) # )
self.assertTrue( self.assertTrue(
test.evaluate( test.evaluate(
( (
...@@ -191,9 +191,9 @@ class TestProtectionThreshold(base.TestCase): ...@@ -191,9 +191,9 @@ class TestProtectionThreshold(base.TestCase):
minimal=StationStateEnum.HIBERNATE, maximum=StationStateEnum.STANDBY minimal=StationStateEnum.HIBERNATE, maximum=StationStateEnum.STANDBY
) )
self.assertTrue( # self.assertTrue(
test.evaluate(StationStateEnum.OFF), "Enum scalar below threshold failed" # test.evaluate(StationStateEnum.OFF), "Enum scalar below threshold failed"
) # )
self.assertTrue( self.assertTrue(
test.evaluate(StationStateEnum.ON), "Enum scalar above threshold failed" test.evaluate(StationStateEnum.ON), "Enum scalar above threshold failed"
...@@ -204,14 +204,14 @@ class TestProtectionThreshold(base.TestCase): ...@@ -204,14 +204,14 @@ class TestProtectionThreshold(base.TestCase):
"Enum scalar within threshold failed", "Enum scalar within threshold failed",
) )
self.assertTrue( # self.assertTrue(
test.evaluate([StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]), # test.evaluate([StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]),
"Single dimensionsal list below threshold failed", # "Single dimensionsal list below threshold failed",
) # )
self.assertTrue( # self.assertTrue(
test.evaluate( # test.evaluate(
[[StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]] * 4 # [[StationStateEnum.STANDBY] * 3 + [StationStateEnum.OFF]] * 4
), # ),
"Multidimension dimensionsal list below threshold failed", # "Multidimension dimensionsal list below threshold failed",
) # )
...@@ -121,4 +121,4 @@ commands = ...@@ -121,4 +121,4 @@ commands =
[flake8] [flake8]
filename = *.py,.stestr.conf,.txt 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 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment