Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Infrastructure / Support

Bugfixes
-----------
* 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 <https://www.github.com/FlexMeasures/flexmeasures/pull/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 <https://www.github.com/FlexMeasures/flexmeasures/pull/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 <https://www.github.com/FlexMeasures/flexmeasures/pull/2367>`_]
* Scheduling jobs no longer print ``Job ... made schedule.`` before ``scheduler.compute()`` runs (only after a successful schedule) [see `PR #2342 <https://www.github.com/FlexMeasures/flexmeasures/pull/2342>`_]
Expand Down
58 changes: 58 additions & 0 deletions flexmeasures/ui/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
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 (
CHARGER_ICON,
FALLBACK_SVG_ICON,
SVG_ICON_MAPPING,
normalize_asset_type_name,
svg_asset_icon_name,
)
from flexmeasures.data.schemas.scheduling import (
UI_FLEX_CONTEXT_SCHEMA,
UI_FLEX_MODEL_SCHEMA,
Expand All @@ -12,6 +19,57 @@
from timely_beliefs.sensors.func_store.knowledge_horizons import x_days_ago_at_y_oclock


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


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_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")
Expand Down
54 changes: 48 additions & 6 deletions flexmeasures/ui/utils/view_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -262,12 +265,47 @@ 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",
"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,
# 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
}


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.

Expand All @@ -285,12 +323,16 @@ 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, "https://api.iconify.design/fa-solid/question-circle.svg"
)
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:
Expand Down
Loading