diff --git a/BlockServer/config/block.py b/BlockServer/config/block.py index 0e5de957..4c8a7f61 100644 --- a/BlockServer/config/block.py +++ b/BlockServer/config/block.py @@ -13,7 +13,8 @@ # along with this program; if not, you can obtain a copy from # https://www.eclipse.org/org/documents/epl-v10.php or # http://opensource.org/licenses/eclipse-1.0.php -from typing import Dict, TypedDict, Union + +from typing import TypedDict from server_common.helpers import PVPREFIX_MACRO @@ -33,6 +34,11 @@ class Block: log_periodic (bool): Whether the block is sampled periodically in the archiver log_rate (float): Time between archive samples (in seconds) log_deadband (float): Deadband for the block to be archived + alarm_enabled (bool): Whether the alarm should be enabled + alarm_latched (bool): Whether the alarm should be latched + alarm_delay (float): The delay for triggering alarm + alarm_guidance (string): The guidance for the alarm + """ def __init__( @@ -51,6 +57,10 @@ def __init__( log_deadband: float = 0, set_block: bool = False, set_block_val: str | None = None, + alarm_enabled: bool = False, + alarm_latched: bool = False, + alarm_delay: float | None = None, + alarm_guidance: str | None = None, ) -> None: """Constructor. @@ -69,6 +79,10 @@ def __init__( log_deadband: Deadband for the block to be archived set_block: whether the block should be set upon config change set_block_val: what the block should be set to upon config change + alarm_enabled: Whether the alarm should be enabled + alarm_latched: Whether the alarm should be latched + alarm_delay: The delay for triggering alarm + alarm_guidance: The guidance for the alarm """ self.name = name self.pv = pv @@ -84,6 +98,10 @@ def __init__( self.log_deadband = log_deadband self.set_block = set_block self.set_block_val = set_block_val + self.alarm_enabled = alarm_enabled + self.alarm_latched = alarm_latched + self.alarm_delay = alarm_delay + self.alarm_guidance = alarm_guidance def _get_pv(self) -> str: pv_name = self.pv @@ -111,7 +129,7 @@ def __str__(self) -> str: f"RCHigh: {self.rc_highlimit}{set_block_str}" ) - def to_dict(self) -> Dict[str, Union[str, float, bool, None]]: + def to_dict(self) -> dict[str, str | float | bool | None]: """Puts the block's details into a dictionary. Returns: @@ -132,6 +150,10 @@ def to_dict(self) -> Dict[str, Union[str, float, bool, None]]: "suspend_on_invalid": self.rc_suspend_on_invalid, "set_block": self.set_block, "set_block_val": self.set_block_val, + "alarm_enabled": self.alarm_enabled, + "alarm_latched": self.alarm_latched, + "alarm_delay": self.alarm_delay, + "alarm_guidance": self.alarm_guidance, } @@ -147,3 +169,7 @@ class BlockKwargs(TypedDict, total=False): log_deadband: float set_block: bool set_block_val: str | None + alarm_enabled: bool | None + alarm_latched: bool | None + alarm_delay: float | None + alarm_guidance: str | None diff --git a/BlockServer/config/xml_converter.py b/BlockServer/config/xml_converter.py index 70f861d7..3884fce3 100644 --- a/BlockServer/config/xml_converter.py +++ b/BlockServer/config/xml_converter.py @@ -13,7 +13,8 @@ # along with this program; if not, you can obtain a copy from # https://www.eclipse.org/org/documents/epl-v10.php or # http://opensource.org/licenses/eclipse-1.0.php -from typing import Dict, List, OrderedDict + +from collections import OrderedDict from xml.dom import minidom from xml.etree import ElementTree @@ -27,6 +28,10 @@ from BlockServer.core.constants import ( GRP_NONE, SIMLEVELS, + TAG_ALARM_DELAY, + TAG_ALARM_ENABLED, + TAG_ALARM_GUIDANCE, + TAG_ALARM_LATCHED, TAG_AUTOSTART, TAG_BLOCK, TAG_BLOCKS, @@ -102,7 +107,7 @@ class ConfigurationXmlConverter: """ @staticmethod - def blocks_to_xml(blocks: OrderedDict, macros: Dict) -> str: + def blocks_to_xml(blocks: OrderedDict, macros: dict) -> str: """Generates an XML representation for a supplied dictionary of blocks. Args: @@ -116,7 +121,7 @@ def blocks_to_xml(blocks: OrderedDict, macros: Dict) -> str: root.attrib["xmlns"] = SCHEMA_PATH + BLOCK_SCHEMA root.attrib["xmlns:blk"] = SCHEMA_PATH + BLOCK_SCHEMA root.attrib["xmlns:xi"] = "http://www.w3.org/2001/XInclude" - for name, block in blocks.items(): + for block in blocks.values(): # Don't save if in component if block.component is None or block.component is False: ConfigurationXmlConverter._block_to_xml(root, block, macros) @@ -144,7 +149,7 @@ def groups_to_xml(groups: OrderedDict, include_none: bool = False) -> str: ConfigurationXmlConverter._group_to_xml(root, group) # If we are adding the None group it should go at the end - if include_none and KEY_NONE in groups.keys(): + if include_none and KEY_NONE in groups: ConfigurationXmlConverter._group_to_xml(root, groups[KEY_NONE]) return minidom.parseString(ElementTree.tostring(root)).toprettyxml() @@ -162,7 +167,7 @@ def iocs_to_xml(iocs: OrderedDict) -> str: root.attrib["xmlns"] = SCHEMA_PATH + IOC_SCHEMA root.attrib["xmlns:ioc"] = SCHEMA_PATH + IOC_SCHEMA root.attrib["xmlns:xi"] = "http://www.w3.org/2001/XInclude" - for name in iocs.keys(): + for name in iocs: # Don't save if in component if iocs[name].component is None: ConfigurationXmlConverter._ioc_to_xml(root, iocs[name]) @@ -182,7 +187,7 @@ def components_to_xml(comps: OrderedDict) -> str: root.attrib["xmlns"] = SCHEMA_PATH + COMPONENT_SCHEMA root.attrib["xmlns:comp"] = SCHEMA_PATH + COMPONENT_SCHEMA root.attrib["xmlns:xi"] = "http://www.w3.org/2001/XInclude" - for name, case_sensitve_name in comps.items(): + for case_sensitve_name in comps.values(): ConfigurationXmlConverter._component_to_xml(root, case_sensitve_name) return minidom.parseString(ElementTree.tostring(root)).toprettyxml() @@ -223,7 +228,7 @@ def meta_to_xml(data: MetaData) -> str: return minidom.parseString(ElementTree.tostring(root)).toprettyxml() @staticmethod - def _block_to_xml(root_xml: ElementTree.Element, block: Block, macros: Dict) -> None: + def _block_to_xml(root_xml: ElementTree.Element, block: Block, macros: dict) -> None: """Generates the XML for a block""" name = block.name read_pv = block.pv @@ -276,6 +281,17 @@ def _block_to_xml(root_xml: ElementTree.Element, block: Block, macros: Dict) -> set_block_val = ElementTree.SubElement(block_xml, TAG_SET_BLOCK_VAL) set_block_val.text = str(block.set_block_val) + # Alarm Config + alarm_enabled = ElementTree.SubElement(block_xml, TAG_ALARM_ENABLED) + alarm_enabled.text = str(block.alarm_enabled) + alarm_latched = ElementTree.SubElement(block_xml, TAG_ALARM_LATCHED) + alarm_latched.text = str(block.alarm_latched) + if block.alarm_delay is not None: + alarm_delay = ElementTree.SubElement(block_xml, TAG_ALARM_DELAY) + alarm_delay.text = str(block.alarm_delay) + alarm_guidance = ElementTree.SubElement(block_xml, TAG_ALARM_GUIDANCE) + alarm_guidance.text = block.alarm_guidance + @staticmethod def _group_to_xml(root_xml: ElementTree.Element, group: Group) -> None: """Generates the XML for a group""" @@ -414,6 +430,28 @@ def blocks_from_xml( if set_block_val is not None: blocks[name.lower()].set_block_val = set_block_val.text + # Alarm Config + alarm_enabled = ConfigurationXmlConverter._find_single_node( + b, NS_TAG_BLOCK, TAG_ALARM_ENABLED + ) + if alarm_enabled is not None: + blocks[name.lower()].alarm_enabled = alarm_enabled.text == "True" + alarm_latched = ConfigurationXmlConverter._find_single_node( + b, NS_TAG_BLOCK, TAG_ALARM_LATCHED + ) + if alarm_latched is not None: + blocks[name.lower()].alarmlatched = alarm_latched.text == "True" + alarm_delay = ConfigurationXmlConverter._find_single_node( + b, NS_TAG_BLOCK, TAG_ALARM_DELAY + ) + if alarm_delay is not None and alarm_delay.text is not None: + blocks[name.lower()].alarm_delay = float(alarm_delay.text) + alarm_guidance = ConfigurationXmlConverter._find_single_node( + b, NS_TAG_BLOCK, TAG_ALARM_GUIDANCE + ) + if alarm_guidance is not None: + blocks[name.lower()].alarm_guidance = alarm_guidance.text + @staticmethod def groups_from_xml( root_xml: ElementTree.Element, groups: OrderedDict, blocks: OrderedDict @@ -436,7 +474,7 @@ def groups_from_xml( gname_low = gname.lower() # Add the group to the dict unless it already exists (i.e. the group is defined twice) - if gname_low not in groups.keys(): + if gname_low not in groups: groups[gname_low] = Group(gname, gcomp) blks = ConfigurationXmlConverter._find_all_nodes(g, NS_TAG_GROUP, TAG_BLOCK) @@ -448,7 +486,7 @@ def groups_from_xml( # Unlikely, but may be a config was edited by hand... if name not in groups[gname_low].blocks: groups[gname_low].blocks.append(name) - if name.lower() in blocks.keys(): + if name.lower() in blocks: blocks[name.lower()].group = gname # Remove the block from the NONE group @@ -506,8 +544,8 @@ def ioc_from_xml(root_xml: ElementTree.Element, iocs: OrderedDict) -> None: iocs[n.upper()].pvsets[ps.attrib[TAG_NAME]] = { TAG_ENABLED: parse_boolean(str(ps.attrib[TAG_ENABLED])) } - except Exception as err: - raise Exception("Tag not found in ioc.xml (" + str(err) + ")") + except (NodeNotPresentError, KeyError, ValueError, SyntaxError) as err: + raise ValueError("Tag not found in ioc.xml (" + str(err) + ")") @staticmethod def components_from_xml(root_xml: ElementTree.Element, components: OrderedDict) -> None: @@ -578,10 +616,11 @@ def meta_from_xml(root_xml: ElementTree.Element, data: MetaData) -> None: @staticmethod def _find_all_nodes( root: ElementTree.Element, tag: str, name: str - ) -> List[ElementTree.Element]: + ) -> list[ElementTree.Element]: """Finds all the nodes regardless of whether it has a namespace or not. - For example the name space for IOCs is xmlns:ioc="http://epics.isis.rl.ac.uk/schema/iocs/1.0" + For example the name space for IOCs is + xmlns:ioc="http://epics.isis.rl.ac.uk/schema/iocs/1.0" Args: root: The XML tree object @@ -641,7 +680,7 @@ def _find_single_node_with_none_check( return node @staticmethod - def _display(child: ElementTree.Element, index: int) -> Dict[str, str | int | None]: + def _display(child: ElementTree.Element, index: int) -> dict[str, str | int | None]: return { "index": index, "name": ConfigurationXmlConverter._find_single_node_with_none_check( @@ -659,7 +698,7 @@ def _display(child: ElementTree.Element, index: int) -> Dict[str, str | int | No } @staticmethod - def _button(child: ElementTree.Element, index: int) -> Dict[str, str | int | None]: + def _button(child: ElementTree.Element, index: int) -> dict[str, str | int | None]: return { "index": index, "name": ConfigurationXmlConverter._find_single_node_with_none_check( @@ -694,7 +733,7 @@ def _button(child: ElementTree.Element, index: int) -> Dict[str, str | int | Non @staticmethod def banner_config_from_xml( root: ElementTree.Element, - ) -> Dict[str, List[Dict[str, str | int | None]]]: + ) -> dict[str, list[dict[str, str | int | None]]]: """ Parses the banner config XML to produce a banner config dictionary @@ -714,16 +753,14 @@ def banner_config_from_xml( banner_buttons = [] items = ConfigurationXmlConverter._find_single_node_with_none_check(root, "banner", "items") - index = 0 - for item in items: + for index, item in enumerate(items): child = item.find("./") if child is not None: if "display" in child.tag: banner_displays.append(ConfigurationXmlConverter._display(child, index)) else: banner_buttons.append(ConfigurationXmlConverter._button(child, index)) - index += 1 return { "items": banner_displays, diff --git a/BlockServer/core/constants.py b/BlockServer/core/constants.py index b5528dd9..0da8a1c7 100644 --- a/BlockServer/core/constants.py +++ b/BlockServer/core/constants.py @@ -82,3 +82,9 @@ FILENAME_GLOBALS = "globals.txt" SCHEMA_FOR = [FILENAME_BLOCKS, FILENAME_GROUPS, FILENAME_IOCS, FILENAME_COMPONENTS, FILENAME_META] + +# Alarm element nodes +TAG_ALARM_ENABLED = "alarm_enabled" +TAG_ALARM_LATCHED = "alarm_latched" +TAG_ALARM_DELAY = "alarm_delay" +TAG_ALARM_GUIDANCE = "alarm_guidance" diff --git a/BlockServer/test_modules/test_configuration_xml.py b/BlockServer/test_modules/test_configuration_xml.py index 8f8bda98..54ec8421 100644 --- a/BlockServer/test_modules/test_configuration_xml.py +++ b/BlockServer/test_modules/test_configuration_xml.py @@ -19,13 +19,14 @@ from collections import OrderedDict from xml.etree import ElementTree +from server_common.helpers import MACROS + from BlockServer.config.block import Block from BlockServer.config.configuration import Configuration from BlockServer.config.group import Group from BlockServer.config.ioc import IOC from BlockServer.config.metadata import MetaData from BlockServer.config.xml_converter import ConfigurationXmlConverter -from server_common.helpers import MACROS BLOCKS_XML = """ @@ -44,6 +45,9 @@ 0 False None + False + False + TESTBLOCK2 @@ -59,6 +63,9 @@ 0 False None + False + False + TESTBLOCK3 @@ -74,6 +81,9 @@ 0 False None + False + False + TESTBLOCK4 @@ -89,6 +99,9 @@ 0 False None + False + False + """