From dd57cbcf1b34642d06677cf9044cd03233982d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Wed, 5 Aug 2026 00:56:13 +0200 Subject: [PATCH 1/6] fix icon mapping for asset types one-way_evse and two-way_evse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- documentation/changelog.rst | 1 + flexmeasures/ui/tests/test_utils.py | 8 ++++++++ flexmeasures/ui/utils/view_utils.py | 2 ++ 3 files changed, 11 insertions(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index a734982fe4..022d8b53d1 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -80,6 +80,7 @@ Infrastructure / Support Bugfixes ----------- +* Show charger icons for one-way and two-way EVSE assets in the UI's asset structure view [see `PR #XXXX `_] * Replaying a chart for a past window no longer shows annotations that were only recorded later; annotation searches and the ``chart_annotations`` endpoints can now be scoped by recording (belief) time [see `PR #2367 `_] * Continuing the query-parameter cleanup started in PR #2352: the chart-related endpoints now use ``prior``, ``start``, ``end`` and hyphenated field names, with a new ``duration`` field to derive a missing ``start``/``end``; old spellings keep working as legacy aliases [see `PR #2367 `_] * Scheduling jobs no longer print ``Job ... made schedule.`` before ``scheduler.compute()`` runs (only after a successful schedule) [see `PR #2342 `_] diff --git a/flexmeasures/ui/tests/test_utils.py b/flexmeasures/ui/tests/test_utils.py index 425b41fd6b..537830c2d8 100644 --- a/flexmeasures/ui/tests/test_utils.py +++ b/flexmeasures/ui/tests/test_utils.py @@ -3,6 +3,7 @@ from flexmeasures import Asset, AssetType, Account, Sensor from flexmeasures.data.models.generic_assets import GenericAsset from flexmeasures.ui.utils.breadcrumb_utils import get_ancestry +from flexmeasures.ui.utils.view_utils import svg_asset_icon_name from flexmeasures.data.schemas.scheduling import ( UI_FLEX_CONTEXT_SCHEMA, UI_FLEX_MODEL_SCHEMA, @@ -12,6 +13,13 @@ from timely_beliefs.sensors.func_store.knowledge_horizons import x_days_ago_at_y_oclock +def test_svg_asset_icon_name_for_evse(): + charger_icon = "https://api.iconify.design/material-symbols/ev-station-outline.svg" + + for asset_type_name in ("one-way_evse", "two-way_evse"): + assert svg_asset_icon_name(asset_type_name) == charger_icon + + def test_get_ancestry(app, db): account = Account(name="Test Account") asset_type = AssetType(name="TestAssetType") diff --git a/flexmeasures/ui/utils/view_utils.py b/flexmeasures/ui/utils/view_utils.py index 3ffd07764c..0126e27a84 100644 --- a/flexmeasures/ui/utils/view_utils.py +++ b/flexmeasures/ui/utils/view_utils.py @@ -264,6 +264,8 @@ def _minimal_ext_cmd(cmd: list): "solar": "https://api.iconify.design/wi/day-sunny.svg", "chargepoint": "https://api.iconify.design/material-symbols/ev-station-outline.svg", "ev": "https://api.iconify.design/material-symbols/ev-station-outline.svg", + "one-way_evse": "https://api.iconify.design/material-symbols/ev-station-outline.svg", + "two-way_evse": "https://api.iconify.design/material-symbols/ev-station-outline.svg", "add_asset": "https://api.iconify.design/material-symbols/add-rounded.svg?color=white", # Plus Icon for Add Asset } From 634a986e486515fdad9a3fb72dd5a2290802b302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Wed, 5 Aug 2026 00:59:02 +0200 Subject: [PATCH 2/6] PR number in changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 022d8b53d1..6e2215553f 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -80,7 +80,7 @@ Infrastructure / Support Bugfixes ----------- -* Show charger icons for one-way and two-way EVSE assets in the UI's asset structure view [see `PR #XXXX `_] +* Show charger icons for one-way and two-way EVSE assets in the UI's asset structure view [see `PR #2391 `_] * Replaying a chart for a past window no longer shows annotations that were only recorded later; annotation searches and the ``chart_annotations`` endpoints can now be scoped by recording (belief) time [see `PR #2367 `_] * Continuing the query-parameter cleanup started in PR #2352: the chart-related endpoints now use ``prior``, ``start``, ``end`` and hyphenated field names, with a new ``duration`` field to derive a missing ``start``/``end``; old spellings keep working as legacy aliases [see `PR #2367 `_] * Scheduling jobs no longer print ``Job ... made schedule.`` before ``scheduler.compute()`` runs (only after a successful schedule) [see `PR #2342 `_] From 0915160016273d1ad0468ea4a576c3762c0f18a3 Mon Sep 17 00:00:00 2001 From: Mohamed Belhsan Hmida Date: Thu, 6 Aug 2026 14:17:14 +0100 Subject: [PATCH 3/6] ui: show icons for more asset types in the asset structure view The structure view fell back to a question mark for any asset type missing from SVG_ICON_MAPPING. Three of the eight asset types that FlexMeasures seeds by default were missing (wind, process and heat-storage), as was EV infrastructure under names other than the two canonical EVSE ones. Add those three, and map the EV names that projects commonly use. Introduce a CHARGER_ICON constant, since the charger URL is now shared by several entries, and a FALLBACK_SVG_ICON constant for the question mark, so tests can assert against it. Co-Authored-By: Claude Opus 5 --- flexmeasures/ui/tests/test_utils.py | 32 +++++++++++++++++++++++++---- flexmeasures/ui/utils/view_utils.py | 28 ++++++++++++++++++------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/flexmeasures/ui/tests/test_utils.py b/flexmeasures/ui/tests/test_utils.py index 537830c2d8..34bb26afb2 100644 --- a/flexmeasures/ui/tests/test_utils.py +++ b/flexmeasures/ui/tests/test_utils.py @@ -3,7 +3,11 @@ from flexmeasures import Asset, AssetType, Account, Sensor from flexmeasures.data.models.generic_assets import GenericAsset from flexmeasures.ui.utils.breadcrumb_utils import get_ancestry -from flexmeasures.ui.utils.view_utils import svg_asset_icon_name +from flexmeasures.ui.utils.view_utils import ( + CHARGER_ICON, + FALLBACK_SVG_ICON, + svg_asset_icon_name, +) from flexmeasures.data.schemas.scheduling import ( UI_FLEX_CONTEXT_SCHEMA, UI_FLEX_MODEL_SCHEMA, @@ -13,11 +17,31 @@ from timely_beliefs.sensors.func_store.knowledge_horizons import x_days_ago_at_y_oclock -def test_svg_asset_icon_name_for_evse(): - charger_icon = "https://api.iconify.design/material-symbols/ev-station-outline.svg" +def test_svg_asset_icon_name_for_default_asset_types(): + """Every asset type that FlexMeasures seeds by default should have its own icon. + + These are the types added by flexmeasures.data.scripts.data_gen.add_default_asset_types. + """ + for asset_type_name in ( + "solar", + "wind", + "one-way_evse", + "two-way_evse", + "battery", + "building", + "process", + "heat-storage", + ): + assert svg_asset_icon_name(asset_type_name) != FALLBACK_SVG_ICON for asset_type_name in ("one-way_evse", "two-way_evse"): - assert svg_asset_icon_name(asset_type_name) == charger_icon + assert svg_asset_icon_name(asset_type_name) == CHARGER_ICON + + +def test_svg_asset_icon_name_falls_back_for_unknown_asset_type(): + assert svg_asset_icon_name("no-such-asset-type") == FALLBACK_SVG_ICON + assert svg_asset_icon_name("") == FALLBACK_SVG_ICON + assert svg_asset_icon_name(None) == FALLBACK_SVG_ICON def test_get_ancestry(app, db): diff --git a/flexmeasures/ui/utils/view_utils.py b/flexmeasures/ui/utils/view_utils.py index 0126e27a84..5b586f4eee 100644 --- a/flexmeasures/ui/utils/view_utils.py +++ b/flexmeasures/ui/utils/view_utils.py @@ -253,6 +253,9 @@ def _minimal_ext_cmd(cmd: list): "wind speed": "wi wi-strong-wind", } +CHARGER_ICON = "https://api.iconify.design/material-symbols/ev-station-outline.svg" +FALLBACK_SVG_ICON = "https://api.iconify.design/fa-solid/question-circle.svg" + SVG_ICON_MAPPING = { # site structure "building": "https://api.iconify.design/mdi/home-city.svg", @@ -262,10 +265,19 @@ def _minimal_ext_cmd(cmd: list): "scenario": "https://api.iconify.design/mdi/binoculars.svg", "pv": "https://api.iconify.design/wi/day-sunny.svg", "solar": "https://api.iconify.design/wi/day-sunny.svg", - "chargepoint": "https://api.iconify.design/material-symbols/ev-station-outline.svg", - "ev": "https://api.iconify.design/material-symbols/ev-station-outline.svg", - "one-way_evse": "https://api.iconify.design/material-symbols/ev-station-outline.svg", - "two-way_evse": "https://api.iconify.design/material-symbols/ev-station-outline.svg", + "wind": "https://api.iconify.design/mdi/wind-turbine.svg", + "process": "https://api.iconify.design/mdi/factory.svg", + "heat-storage": "https://api.iconify.design/mdi/storage-tank.svg", + # EV infrastructure, which projects name in a variety of ways + "chargepoint": CHARGER_ICON, + "ev": CHARGER_ICON, + "evse": CHARGER_ICON, + "one-way_evse": CHARGER_ICON, + "two-way_evse": CHARGER_ICON, + "charging_station": CHARGER_ICON, + "charging_hub": CHARGER_ICON, + "ev_charger": CHARGER_ICON, + "charger": CHARGER_ICON, "add_asset": "https://api.iconify.design/material-symbols/add-rounded.svg?color=white", # Plus Icon for Add Asset } @@ -287,12 +299,14 @@ def asset_icon_name(asset_type_name: str) -> str: def svg_asset_icon_name(asset_type_name: str) -> str: + """SVG icon URL for this asset type, as used in the asset structure view. + A namespaced name, such as a plugin's "myplugin.battery", is matched on its last dot-separated component. + An asset type we have no icon for falls back to a question mark. + """ if asset_type_name: asset_type_name = asset_type_name.split(".")[-1].lower() - return SVG_ICON_MAPPING.get( - asset_type_name, "https://api.iconify.design/fa-solid/question-circle.svg" - ) + return SVG_ICON_MAPPING.get(asset_type_name, FALLBACK_SVG_ICON) def username(user_id) -> str: From 3f389eb871b9b5f012ef35e6b9d27485255afcd3 Mon Sep 17 00:00:00 2001 From: Mohamed Belhsan Hmida Date: Thu, 6 Aug 2026 14:17:53 +0100 Subject: [PATCH 4/6] ui: match asset type names ignoring case and separators Asset type names are not restricted, so projects spell the same type in their own way. The Campus project, for instance, types its chargers as 'charge-point' and 'charging-station', which found no icon even though SVG_ICON_MAPPING already had a 'chargepoint' entry. Look up icons by a normalized name, keeping only lower-case alphanumeric characters, so that separator and case variants share one entry. The dot-separated split is kept and applied first, so a plugin's namespaced type (for example 'myplugin.battery') still matches on its last component. A test guards against two mapping keys normalizing to the same name, which would silently shadow each other. Co-Authored-By: Claude Opus 5 --- flexmeasures/ui/tests/test_utils.py | 26 ++++++++++++++++++++++++++ flexmeasures/ui/utils/view_utils.py | 22 ++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/flexmeasures/ui/tests/test_utils.py b/flexmeasures/ui/tests/test_utils.py index 34bb26afb2..3e499ec9e3 100644 --- a/flexmeasures/ui/tests/test_utils.py +++ b/flexmeasures/ui/tests/test_utils.py @@ -6,6 +6,8 @@ from flexmeasures.ui.utils.view_utils import ( CHARGER_ICON, FALLBACK_SVG_ICON, + SVG_ICON_MAPPING, + normalize_asset_type_name, svg_asset_icon_name, ) from flexmeasures.data.schemas.scheduling import ( @@ -44,6 +46,30 @@ def test_svg_asset_icon_name_falls_back_for_unknown_asset_type(): assert svg_asset_icon_name(None) == FALLBACK_SVG_ICON +def test_svg_asset_icon_name_ignores_case_and_separators(): + """Projects are free to name their asset types, so spelling variants should find the same icon.""" + for asset_type_name in ("charge-point", "charge point", "Charge_Point"): + assert svg_asset_icon_name(asset_type_name) == CHARGER_ICON + + # a namespaced asset type, as a plugin may define it, is matched on its last component + assert svg_asset_icon_name("myplugin.battery") == svg_asset_icon_name("battery") + + +def test_svg_icon_mapping_keys_do_not_collide_when_normalized(): + """Two keys that normalize to the same name would silently shadow each other.""" + normalized_keys: dict[str, list[str]] = {} + for asset_type_name in SVG_ICON_MAPPING: + normalized_keys.setdefault( + normalize_asset_type_name(asset_type_name), [] + ).append(asset_type_name) + + assert { + normalized: keys + for normalized, keys in normalized_keys.items() + if len(keys) > 1 + } == {} + + def test_get_ancestry(app, db): account = Account(name="Test Account") asset_type = AssetType(name="TestAssetType") diff --git a/flexmeasures/ui/utils/view_utils.py b/flexmeasures/ui/utils/view_utils.py index 5b586f4eee..f5d27e15d8 100644 --- a/flexmeasures/ui/utils/view_utils.py +++ b/flexmeasures/ui/utils/view_utils.py @@ -282,6 +282,22 @@ def _minimal_ext_cmd(cmd: list): } +def normalize_asset_type_name(asset_type_name: str) -> str: + """Reduce an asset type name to its lower-case alphanumeric characters, so that spelling variants share one mapping key. + + For example, "charge-point", "charge point" and "Charge_Point" all reduce to "chargepoint". + """ + return "".join( + character for character in asset_type_name.lower() if character.isalnum() + ) + + +NORMALIZED_SVG_ICON_MAPPING = { + normalize_asset_type_name(asset_type_name): icon + for asset_type_name, icon in SVG_ICON_MAPPING.items() +} + + def asset_icon_name(asset_type_name: str) -> str: """Icon name for this asset type. @@ -301,12 +317,14 @@ def asset_icon_name(asset_type_name: str) -> str: def svg_asset_icon_name(asset_type_name: str) -> str: """SVG icon URL for this asset type, as used in the asset structure view. + Asset type names are not restricted, so projects spell the same type in different ways. + Matching therefore ignores case and separators, letting a type named "charge-point" share the icon mapped to "chargepoint". A namespaced name, such as a plugin's "myplugin.battery", is matched on its last dot-separated component. An asset type we have no icon for falls back to a question mark. """ if asset_type_name: - asset_type_name = asset_type_name.split(".")[-1].lower() - return SVG_ICON_MAPPING.get(asset_type_name, FALLBACK_SVG_ICON) + asset_type_name = normalize_asset_type_name(asset_type_name.split(".")[-1]) + return NORMALIZED_SVG_ICON_MAPPING.get(asset_type_name, FALLBACK_SVG_ICON) def username(user_id) -> str: From 59d1577832d36dcc9e097d0180fe999ed84fd2ce Mon Sep 17 00:00:00 2001 From: Mohamed Belhsan Hmida Date: Thu, 6 Aug 2026 14:17:53 +0100 Subject: [PATCH 5/6] ui: show icons for building services equipment The Campus project types most of its assets as building services equipment (hvac, ahu, dhw, heatpump, chiller, lighting and other-loads), which had no icon and so filled its asset structure view with question marks. These are not FlexMeasures asset types, but projects are free to name their asset types, and these abbreviations are the common ones in building services. Normalization means one 'heatpump' entry also covers the 'heat-pump' and 'heat_pump' spellings. Co-Authored-By: Claude Opus 5 --- flexmeasures/ui/utils/view_utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flexmeasures/ui/utils/view_utils.py b/flexmeasures/ui/utils/view_utils.py index f5d27e15d8..6954181a27 100644 --- a/flexmeasures/ui/utils/view_utils.py +++ b/flexmeasures/ui/utils/view_utils.py @@ -278,6 +278,14 @@ def _minimal_ext_cmd(cmd: list): "charging_hub": CHARGER_ICON, "ev_charger": CHARGER_ICON, "charger": CHARGER_ICON, + # building services equipment, named by their common abbreviations + "hvac": "https://api.iconify.design/mdi/hvac.svg", + "ahu": "https://api.iconify.design/mdi/air-filter.svg", # air handling unit + "dhw": "https://api.iconify.design/mdi/water-boiler.svg", # domestic hot water + "heatpump": "https://api.iconify.design/mdi/heat-pump.svg", + "chiller": "https://api.iconify.design/mdi/snowflake.svg", + "lighting": "https://api.iconify.design/mdi/lightbulb.svg", + "other-loads": "https://api.iconify.design/mdi/power-plug.svg", "add_asset": "https://api.iconify.design/material-symbols/add-rounded.svg?color=white", # Plus Icon for Add Asset } From b9b2a9402051ccbbd38dda5d96135c58d0d6f426 Mon Sep 17 00:00:00 2001 From: Mohamed Belhsan Hmida Date: Thu, 6 Aug 2026 14:18:03 +0100 Subject: [PATCH 6/6] docs: add changelog entry for the asset icon fixes Co-Authored-By: Claude Opus 5 --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 6e2215553f..599cd295f7 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -80,7 +80,7 @@ Infrastructure / Support Bugfixes ----------- -* Show charger icons for one-way and two-way EVSE assets in the UI's asset structure view [see `PR #2391 `_] +* Show icons for more asset types in the UI's asset structure view, which previously fell back to a question mark: the ``wind``, ``process`` and ``heat-storage`` types that FlexMeasures seeds by default, and EV infrastructure under its various names (such as ``one-way_evse``, ``two-way_evse``, ``evse``, ``charging_station`` and ``charging_hub``) and building services equipment (``hvac``, ``ahu``, ``dhw``, ``heatpump``, ``chiller``, ``lighting`` and ``other-loads``). Asset type names are now matched ignoring case and separators, so an asset type named ``charge-point`` gets the same icon as ``chargepoint`` [see `PR #2391 `_] * Replaying a chart for a past window no longer shows annotations that were only recorded later; annotation searches and the ``chart_annotations`` endpoints can now be scoped by recording (belief) time [see `PR #2367 `_] * Continuing the query-parameter cleanup started in PR #2352: the chart-related endpoints now use ``prior``, ``start``, ``end`` and hyphenated field names, with a new ``duration`` field to derive a missing ``start``/``end``; old spellings keep working as legacy aliases [see `PR #2367 `_] * Scheduling jobs no longer print ``Job ... made schedule.`` before ``scheduler.compute()`` runs (only after a successful schedule) [see `PR #2342 `_]