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 18d84909..0e8e286b 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -158,6 +158,11 @@ 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"): + component_name = entity["type"] + entity["type"] = "fastcs*" + if entity["type"] in self.techui_support.support_modules: support_mapping: SupportEntity = ( self.techui_support.support_modules[entity["type"]] @@ -178,10 +183,12 @@ 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] self.entities[pv_root].append(new_entity) + break def _generate_screen(self, screen_name: str): @@ -216,6 +223,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 b1dee7f9..55f0db6d 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -4,8 +4,10 @@ from collections import defaultdict from collections.abc import Mapping 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 @@ -42,13 +44,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 @@ -162,7 +168,11 @@ def _update_macros(self, component: Entity) -> tuple[str, dict[str, str]]: suffix_key = next(k for k, v in component.macros.items() if v == suffix) except (IndexError, ValueError): prefix = component.prefix - component_name = component.type + 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, @@ -189,23 +199,35 @@ def _allocate_widget( # Get relative path to screen file = Template(screen_mapping["file"]).render(component.macros) + + # IF the file starts with IOC, and needs macro expansion if file.startswith("$(IOC)"): screen_path = support_screen_path = file.replace( "$(IOC)", f"{self.beamline_url}/{component.service_name}" - ) # Only works with related displays as - # embedded displays need to access the file to get dimensions - - 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: - 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 a suffix field # to the components, and adjust the name and suffix accordingly @@ -228,7 +250,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), @@ -283,6 +305,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 2cfc27cc..1a2277b8 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -306,6 +306,14 @@ 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 + name: Annotated[ + str | None, + Field(description="placeholder name for fastcs screens"), + ] = None class SupportEntity(BaseModel): diff --git a/tests/conftest.py b/tests/conftest.py index 28eb8175..985f8765 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,9 +102,7 @@ def techui_support(): }, ], ), - "fastcs.TemperatureController": SupportEntity( - prefix="{{ name }}", macros=["name"], screens=[{}] - ), + "fastcs*": SupportEntity(prefix="{{ id }}", macros=["id"], screens=[{}]), } return ts 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