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
+
"""