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
25 changes: 23 additions & 2 deletions .github/mutations/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
}
]
4 changes: 2 additions & 2 deletions custom_components/weishaupt_modbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions custom_components/weishaupt_modbus/webif_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down
32 changes: 30 additions & 2 deletions custom_components/weishaupt_modbus/webif_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions custom_components/weishaupt_modbus/webif_visit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -148,7 +148,7 @@ async def _visit(client: Client) -> Visited:
found,
{},
REQUIRED_TITLES[model],
ENGLISH_TITLES[model],
TITLES_SHOWN_OTHERWISE[model],
OPTIONAL_TITLES[model],
)
}
Expand Down
17 changes: 14 additions & 3 deletions tests/test_webif_captures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
)
Expand Down
38 changes: 38 additions & 0 deletions tests/test_webif_evoblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand Down
Loading