diff --git a/.github/mutations/plan.json b/.github/mutations/plan.json index 81bd850..faac566 100644 --- a/.github/mutations/plan.json +++ b/.github/mutations/plan.json @@ -3649,14 +3649,14 @@ { "label": "webif english: the running entry polls without the English titles", "path": "custom_components/weishaupt_modbus/__init__.py", - "old": " ENGLISH_TITLES[model],\n", + "old": " TITLES_SHOWN_OTHERWISE[model],\n", "new": " {key: {} for key in REQUIRED_TITLES[model]},\n", "tests": "become_sensors_on_a_device or power_limit_is_set_from_home_assistant" }, { "label": "webif english: the dialog reads the pages without the English titles", "path": "custom_components/weishaupt_modbus/webif_visit.py", - "old": " ENGLISH_TITLES[model],\n", + "old": " TITLES_SHOWN_OTHERWISE[model],\n", "new": " {key: {} for key in REQUIRED_TITLES[model]},\n", "tests": "reads_each_page_it_found_once" }, @@ -5199,5 +5199,26 @@ "old": " state_class=SensorStateClass.MEASUREMENT,\n suggested_display_precision=2,\n", "new": " suggested_display_precision=2,\n", "tests": "running_total_is_kept" + }, + { + "label": "webif spelling: a WBB knows the heat exchanger cut only", + "path": "custom_components/weishaupt_modbus/webif_sensor.py", + "old": " other_titles=(HEAT_EXCHANGER_IN_FULL,),\n", + "new": "", + "tests": "spelling_the_heat_exchanger_out" + }, + { + "label": "webif spelling: an Evoblock knows the heat exchanger in full only", + "path": "custom_components/weishaupt_modbus/webif_sensor.py", + "old": " other_titles=(HEAT_EXCHANGER_CUT,),\n", + "new": "", + "tests": "cutting_the_heat_exchanger_title" + }, + { + "label": "webif spelling: no page takes another spelling", + "path": "custom_components/weishaupt_modbus/webif_sensor.py", + "old": " for value in VALUES[model]\n if value.page == page\n for other in value.other_titles\n", + "new": " for value in ()\n for other in value.other_titles\n", + "tests": "spelling_the_heat_exchanger_out or cutting_the_heat_exchanger_title" } ] diff --git a/custom_components/weishaupt_modbus/__init__.py b/custom_components/weishaupt_modbus/__init__.py index 9f994e7..b9d71c4 100644 --- a/custom_components/weishaupt_modbus/__init__.py +++ b/custom_components/weishaupt_modbus/__init__.py @@ -50,10 +50,10 @@ from .webif.client import Client from .webif_coordinator import WebifCoordinator, polled_pages from .webif_sensor import ( - ENGLISH_TITLES, OPTIONAL_TITLES, POWER_LIMIT, REQUIRED_TITLES, + TITLES_SHOWN_OTHERWISE, entry_model, ) from .weishaupt_modbus_api.const import DEFAULT_PORT, MODBUS_UNIT_ID @@ -275,7 +275,7 @@ def follow_pump(change: ConfigEntryChange, changed: ConfigEntry) -> None: entry.data[CONF.PAGES], entry.options, REQUIRED_TITLES[model], - ENGLISH_TITLES[model], + TITLES_SHOWN_OTHERWISE[model], OPTIONAL_TITLES[model], ) # No warning: the daily limit is the guard for settings written here. diff --git a/custom_components/weishaupt_modbus/webif_coordinator.py b/custom_components/weishaupt_modbus/webif_coordinator.py index 9957c9e..b1e75a4 100644 --- a/custom_components/weishaupt_modbus/webif_coordinator.py +++ b/custom_components/weishaupt_modbus/webif_coordinator.py @@ -104,8 +104,8 @@ class Page: """A page to keep fresh: where it is, what it must show, how often. A menu page is read for the values its entries show one level down. - `english` maps the titles a controller set to English shows to the - German ones everything else goes by. `optional` are titles read when the + `english` maps the titles a controller shows otherwise, set to English or + spelt another way, to the German ones everything else goes by. `optional` are titles read when the page shows them, which it need not. """ diff --git a/custom_components/weishaupt_modbus/webif_sensor.py b/custom_components/weishaupt_modbus/webif_sensor.py index 4d7423f..9f631ed 100644 --- a/custom_components/weishaupt_modbus/webif_sensor.py +++ b/custom_components/weishaupt_modbus/webif_sensor.py @@ -60,6 +60,10 @@ ONLY_WBB = frozenset({WBB}) ONLY_EVOBLOCK = frozenset({EVOBLOCK}) EVOBLOCK_VERSION_TITLE = "Version WEB CPU" +# One WBB with one firmware cut this title on 2026-10-04 and spelt it out on +# 2026-11-08, both in German; an Evoblock spelt it out. +HEAT_EXCHANGER_CUT = "Wärmetauscher AG Austrit" +HEAT_EXCHANGER_IN_FULL = "Wärmetauscher AG Austritt" # Only a WEB 13/20 set to German was seen, 2026-11-05 and 06: what its own # values are called in English is not known. NOT_SEEN = "" @@ -72,6 +76,8 @@ class WebifSensorDescription(SensorEntityDescription): `english_title` is the title a controller set to English shows instead, as it showed it on 2026-10-10: cut at 25 characters, typing errors kept; NOT_SEEN for a value no controller set to English has shown. + `other_titles` are titles a controller showed for the same row in its + own language, spelt otherwise. `shown_unit` is the text after the number; None for a value that is text. `topic` is the pump device whose short name the name prefix option puts in front, as for the Modbus sensors of the same subject. @@ -94,6 +100,7 @@ class WebifSensorDescription(SensorEntityDescription): topic: str = DEVICES.WP models: frozenset[str] = frozenset(MODELS) conditional_on: frozenset[str] = frozenset() + other_titles: tuple[str, ...] = () off_is_zero: bool = False no_value_is_zero: bool = False @@ -289,15 +296,17 @@ def _performance(key: str, title: str, english_title: str) -> WebifSensorDescrip ), _temperature( "waermetauscher_ag_austritt", - "Wärmetauscher AG Austrit", + HEAT_EXCHANGER_CUT, "OUTLET TEMP", models=ONLY_WBB, + other_titles=(HEAT_EXCHANGER_IN_FULL,), ), _temperature( "waermetauscher_ag_austritt", - "Wärmetauscher AG Austritt", + HEAT_EXCHANGER_IN_FULL, NOT_SEEN, models=ONLY_EVOBLOCK, + other_titles=(HEAT_EXCHANGER_CUT,), ), _temperature( "evi_sauggastemperatur", @@ -493,6 +502,25 @@ def _performance(key: str, title: str, english_title: str) -> WebifSensorDescrip for model, values in VALUES.items() } +# Every title a page may show for a value but the one it is kept under: +# its English title, and its other spellings. +TITLES_SHOWN_OTHERWISE = { + model: { + page: { + **english, + **{ + other: value.title + for value in VALUES[model] + if value.page == page + for other in value.other_titles + }, + } + for page, english in pages_of_model.items() + } + for model, pages_of_model in ENGLISH_TITLES.items() +} + + # User wish, 2026-10-03: whether the access gets worse over time shows in # Home Assistant's statistics of these. TRAFFIC_SENSORS = tuple( diff --git a/custom_components/weishaupt_modbus/webif_visit.py b/custom_components/weishaupt_modbus/webif_visit.py index e4774f5..635917f 100644 --- a/custom_components/weishaupt_modbus/webif_visit.py +++ b/custom_components/weishaupt_modbus/webif_visit.py @@ -25,10 +25,10 @@ ) from .webif_coordinator import Page, polled_pages from .webif_sensor import ( - ENGLISH_TITLES, MODELS, OPTIONAL_TITLES, REQUIRED_TITLES, + TITLES_SHOWN_OTHERWISE, model_shown, shown_in_unknown_units, ) @@ -148,7 +148,7 @@ async def _visit(client: Client) -> Visited: found, {}, REQUIRED_TITLES[model], - ENGLISH_TITLES[model], + TITLES_SHOWN_OTHERWISE[model], OPTIONAL_TITLES[model], ) } diff --git a/tests/test_webif_captures.py b/tests/test_webif_captures.py index 34abb2a..43da178 100644 --- a/tests/test_webif_captures.py +++ b/tests/test_webif_captures.py @@ -22,9 +22,10 @@ ) from custom_components.weishaupt_modbus.webif_coordinator import polled_pages from custom_components.weishaupt_modbus.webif_sensor import ( - ENGLISH_TITLES, OPTIONAL_TITLES, REQUIRED_TITLES, + TITLES_SHOWN_OTHERWISE, + VALUES, WBB, shown_in_unknown_units, ) @@ -76,7 +77,13 @@ def _titles(capture: pathlib.Path, key: str) -> list[str]: page = _page(capture, key) if "titles" in page: return page["titles"] - return [title for title, _ in page["entries"]] + spelt_otherwise = { + other: value.title + for value in VALUES[WBB] + if value.page == key + for other in value.other_titles + } + return [spelt_otherwise.get(title, title) for title, _ in page["entries"]] def _as_served(key: str, entries: list[list[str]]) -> str: @@ -105,7 +112,11 @@ def test_the_catalogue_reads_the_captured_page(capture, key): page = next( page for page in polled_pages( - PATHS, {}, REQUIRED_TITLES[WBB], ENGLISH_TITLES[WBB], OPTIONAL_TITLES[WBB] + PATHS, + {}, + REQUIRED_TITLES[WBB], + TITLES_SHOWN_OTHERWISE[WBB], + OPTIONAL_TITLES[WBB], ) if page.key == key ) diff --git a/tests/test_webif_evoblock.py b/tests/test_webif_evoblock.py index 42c1900..a7ff098 100644 --- a/tests/test_webif_evoblock.py +++ b/tests/test_webif_evoblock.py @@ -55,6 +55,8 @@ (Path(__file__).parent / "evoblock_titles.json").read_text(encoding="utf-8") ) CONDITIONAL_ROW = "Schaltdifferenz dynamisch" +SHORT_TITLE = "Wärmetauscher AG Austrit" +FULL_TITLE = "Wärmetauscher AG Austritt" # Past every page's interval. ROUND_LATER = 3 * 3600 # A row that shows a word, not a number. @@ -240,6 +242,42 @@ async def test_a_doubled_conditional_row_stops_polling_and_says_which(hass, pump assert stopped[1] == f"Broken (blank or doubled: {CONDITIONAL_ROW})" +def with_title(rows, shown, instead): + return [(instead if title == shown else title, text) for title, text in rows] + + +async def test_a_wbb_spelling_the_heat_exchanger_out_is_whole(hass, pump): + """Its language reset to German after an outage, 2026-11-08, a WBB wrote + "Austritt" in full where on 2026-10-04 it cut it, and all 31 values went.""" + pump.site.update(SITE) + pump.site[PAGES[HEAT_PUMP_PAGE]] = column( + "".join( + value(title, text) + for title, text in with_title( + SHOWN[HEAT_PUMP_PAGE].items(), SHORT_TITLE, FULL_TITLE + ) + ) + ) + + visited = await read_web_interface(hass, pump.host, USER, PASSWORD) + await _start(hass, _entries(hass, pump)) + + assert visited.model == WBB + assert _state(hass, "waermetauscher_ag_austritt") == "21.5" + assert _state(hass, "verdichter_drehzahl") == "2568.0" + + +async def test_an_evoblock_cutting_the_heat_exchanger_title_is_whole(hass, pump): + """The same controller wrote both; an Evoblock may as well.""" + pump.site[PAGES[HEAT_PUMP_PAGE]] = heat_pump_page( + with_title(ROWS[HEAT_PUMP_PAGE], FULL_TITLE, SHORT_TITLE) + ) + + await _evoblock(hass, pump) + + assert _state(hass, "waermetauscher_ag_austritt") == "12.0" + + async def test_dynamic_switching_is_read_while_the_page_shows_it(hass, pump): await _evoblock(hass, pump)