From 0f3a697400808caf254159059bcb1e6eb3934436 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 25 Aug 2026 09:34:14 +0000 Subject: [PATCH 1/7] Added the ability to template for fastcs modules --- src/techui_builder/builder.py | 4 ++++ src/techui_builder/generate.py | 19 +++++++++++++------ src/techui_builder/models.py | 4 ++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 18d84909..e13851f9 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -158,6 +158,9 @@ def _extract_entities(self, service_name: str, service_yaml: Path): entity_key = match.group() for entity in ioc_conf[entity_key]: + if entity["type"].startswith("fastcs"): + entity["type"] = "fastcs*" + if entity["type"] in self.techui_support.support_modules: support_mapping: SupportEntity = ( self.techui_support.support_modules[entity["type"]] @@ -216,6 +219,7 @@ def create_screens(self): # with the same prefix as the component for entity in self.entities[component.prefix]: entity.child_labels = component.child_labels + entity.file = component.file screen_entities.extend(self.entities[component.prefix]) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index fbb3f456..a8516c45 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -201,13 +201,17 @@ def _allocate_widget( "Only related displays can have remote screens" ) else: - screen_path = self.support_path / f"bob/{file}" - logger_.debug(f"Screen path: {screen_path}") + support_bob = (self.support_path / "bob").resolve() + configured_path = Path(file) - # Path of screen relative to synoptic/ - support_screen_path = screen_path.relative_to( - self.synoptic_dir, walk_up=True - ) + if configured_path.is_absolute(): + screen_path = configured_path.resolve() + elif configured_path.parts[:2] == ("techui-support", "bob"): + screen_path = (self.synoptic_dir / configured_path).resolve() + else: + screen_path = (support_bob / configured_path).resolve() + + support_screen_path = screen_path.relative_to(self.synoptic_dir.resolve()) # For Gui Components with multiple components embedded, we add macro overrides # via screen_macros and adjust the name/label accordingly @@ -292,6 +296,9 @@ def _create_widgets( {name}. Skipping..." ) return None + # if component is fastcs, and has the field of file, add it to the support + if component.file: + screen_mapping.append({"file": component.file, "type": "embedded"}) for screen_dict in screen_mapping: new_widget.append(self._allocate_widget(screen_dict, component)) diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 81f50bc8..527c438e 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -306,6 +306,10 @@ class Entity(BaseModel): dict[str, Any], Field(description="Macros for the matching screen (can be empty)"), ] + file: Annotated[ + str | None, + Field(description="File path for custom screen"), + ] = None class SupportEntityScreen(BaseModel): From e14277436b02af29a9157963123caf6fdc0c01c8 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Thu, 27 Aug 2026 08:36:26 +0000 Subject: [PATCH 2/7] Added remote fetching of files to be able to determine their sizes --- src/techui_builder/generate.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index a8516c45..73e66dba 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -3,8 +3,10 @@ import re from collections import defaultdict from dataclasses import dataclass, field +from io import BytesIO from pathlib import Path +import requests from jinja2 import Template from lxml import objectify from phoebusgen import screen as pscreen @@ -41,13 +43,17 @@ class Generator: group_padding: int = field(default=50, init=False, repr=False) label_flag: bool = field(default=False, init=False, repr=False) - def _get_screen_dimensions(self, file: str) -> tuple[int, int]: + def _get_screen_dimensions(self, file: Path | bytes) -> tuple[int, int]: """ Parses the bob files for information on the height and width of the screen """ # Read the bob file - tree = objectify.parse(file) + if isinstance(file, bytes): + tree = objectify.parse(BytesIO(file)) + else: + tree = objectify.parse(str(file)) + root = tree.getroot() try: height_element = root.height @@ -200,6 +206,17 @@ def _allocate_widget( assert screen_mapping.type == "related", ( "Only related displays can have remote screens" ) + # For embedded screens, that need to be placed on screen and dimensions, + # it is required to fetch the screen from remote + if screen_mapping["type"] == "embedded" and str( + support_screen_path + ).startswith("https"): + try: + screen_path = requests.get(str(support_screen_path)).content + except requests.RequestException: + logger_.warning( + f"Could not retrieve file from link {support_screen_path}" + ) else: support_bob = (self.support_path / "bob").resolve() configured_path = Path(file) @@ -241,7 +258,7 @@ def _allocate_widget( pass if screen_mapping.type == "embedded": - height, width = self._get_screen_dimensions(str(screen_path)) + height, width = self._get_screen_dimensions(screen_path) new_widget = pwidget.EmbeddedDisplay( component_name, str(support_screen_path), From 329b839f63ba85307fe67068416aaee3d3810ddf Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Fri, 28 Aug 2026 08:06:29 +0000 Subject: [PATCH 3/7] Ensured the labels are treated correctly for the fastcs screens --- src/techui_builder/builder.py | 2 ++ src/techui_builder/generate.py | 9 ++++++--- src/techui_builder/models.py | 4 ++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index e13851f9..67d424f5 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -159,6 +159,7 @@ def _extract_entities(self, service_name: str, service_yaml: Path): for entity in ioc_conf[entity_key]: if entity["type"].startswith("fastcs"): + entity["name"] = entity["type"] entity["type"] = "fastcs*" if entity["type"] in self.techui_support.support_modules: @@ -185,6 +186,7 @@ def _extract_entities(self, service_name: str, service_yaml: Path): pv_root = prefix.split(":", maxsplit=1)[0] self.entities[pv_root].append(new_entity) + break def _generate_screen(self, screen_name: str): diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 73e66dba..5ba74a2a 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -169,9 +169,12 @@ def _update_macros(self, component: Entity) -> tuple[str, dict[str, str]]: ) except (IndexError, ValueError): prefix = component.prefix - component_name = component.type - suffix_key = None - suffix = "" + component_name = ( + component.name + if component.type == "fastcs*" and component.name is not None + else component.type + ) + suffix_key = suffix = "" # Try to get name from child labels if they exist, # if not, just use the name as it is. diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 527c438e..27cf1f22 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -310,6 +310,10 @@ class Entity(BaseModel): str | None, Field(description="File path for custom screen"), ] = None + name: Annotated[ + str | None, + Field(description="placeholder name for fastcs screens"), + ] = None class SupportEntityScreen(BaseModel): From fa237a1810e5543b0e0c8ea3afe10a42e3c8b27e Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Fri, 28 Aug 2026 12:49:36 +0000 Subject: [PATCH 4/7] Fixed tests --- .../services/bl01t-ea-temp-01/config/fastcs.yaml | 2 +- src/techui_builder/builder.py | 4 +++- tests/conftest.py | 6 +++--- tests/test_builder.py | 8 +++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml b/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml index 790d6aed..a06071fc 100644 --- a/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml +++ b/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=schema.json controllers: - - name: BL01T-EA-TEMP-01 + - id: BL01T-EA-TEMP-01 type: fastcs.TemperatureController ip_settings: ip: "localhost" diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 67d424f5..0e8e286b 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -158,8 +158,9 @@ def _extract_entities(self, service_name: str, service_yaml: Path): entity_key = match.group() for entity in ioc_conf[entity_key]: + component_name = None if entity["type"].startswith("fastcs"): - entity["name"] = entity["type"] + component_name = entity["type"] entity["type"] = "fastcs*" if entity["type"] in self.techui_support.support_modules: @@ -182,6 +183,7 @@ def _extract_entities(self, service_name: str, service_yaml: Path): desc=entity.get("desc", None), prefix=prefix, macros=macros, + name=component_name, ) pv_root = prefix.split(":", maxsplit=1)[0] diff --git a/tests/conftest.py b/tests/conftest.py index 78fbfb5f..d86ad228 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,9 +109,9 @@ def techui_support(): ), ], ), - "fastcs.TemperatureController": SupportEntity( - prefix="{{ name }}", - macros=["name"], + "fastcs*": SupportEntity( + prefix="{{ id }}", + macros=["id"], screens=[SupportEntityScreen(file="", type="")], ), } diff --git a/tests/test_builder.py b/tests/test_builder.py index bc9766f1..f5e64720 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -125,19 +125,20 @@ def test_gb_extract_entities_ioc_yaml( @pytest.mark.parametrize( - "index, type, desc, pv, macros", + "index, type, name, desc, pv, macros", [ ( 0, + "fastcs*", "fastcs.TemperatureController", None, "BL01T-EA-TEMP-01", - {"name": "BL01T-EA-TEMP-01"}, + {"id": "BL01T-EA-TEMP-01"}, ), ], ) def test_gb_extract_entities_fastcs_yaml( - builder, techui_support, index, type, desc, pv, macros + builder, techui_support, index, type, name, desc, pv, macros ): # We don't want to use builder_with_setup as that calls _extract_services() # and in turn that calls _extract_entities() @@ -151,6 +152,7 @@ def test_gb_extract_entities_fastcs_yaml( ) entity = builder.entities[prefix][index] assert entity.type == type + assert entity.name == name assert entity.desc == desc assert entity.prefix == pv assert entity.macros == macros From 34c7949baffb7d5bf6f546550ecefa74121d8706 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 29 Sep 2026 14:06:01 +0000 Subject: [PATCH 5/7] ref: changed from defaulting to fastcs*, to fastcs* as a fallback --- src/techui_builder/builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 0e8e286b..ae68e2fa 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -159,7 +159,9 @@ def _extract_entities(self, service_name: str, service_yaml: Path): for entity in ioc_conf[entity_key]: component_name = None - if entity["type"].startswith("fastcs"): + if (entity["type"].startswith("fastcs")) and ( + entity["type"] not in self.techui_support.support_modules + ): component_name = entity["type"] entity["type"] = "fastcs*" From c9dd6854aa0b8348b0e8bd6c9e235d4cc4ea99de Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 6 Oct 2026 07:42:14 +0000 Subject: [PATCH 6/7] Fix type-checking --- src/techui_builder/generate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 5ba74a2a..5ca16f18 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -211,7 +211,7 @@ def _allocate_widget( ) # For embedded screens, that need to be placed on screen and dimensions, # it is required to fetch the screen from remote - if screen_mapping["type"] == "embedded" and str( + if screen_mapping.type == "embedded" and str( support_screen_path ).startswith("https"): try: @@ -318,7 +318,9 @@ def _create_widgets( return None # if component is fastcs, and has the field of file, add it to the support if component.file: - screen_mapping.append({"file": component.file, "type": "embedded"}) + screen_mapping.append( + SupportEntityScreen(file=component.file, type="embedded") + ) for screen_dict in screen_mapping: new_widget.append(self._allocate_widget(screen_dict, component)) From 99ff33336f57620928fc01614b9abbf4bb691f53 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 6 Oct 2026 07:58:57 +0000 Subject: [PATCH 7/7] Fix suffix and key assigment to be None --- src/techui_builder/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 5ca16f18..c3d575a4 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -174,7 +174,7 @@ def _update_macros(self, component: Entity) -> tuple[str, dict[str, str]]: if component.type == "fastcs*" and component.name is not None else component.type ) - suffix_key = suffix = "" + suffix_key = suffix = None # Try to get name from child labels if they exist, # if not, just use the name as it is.