diff --git a/CHANGELOG.md b/CHANGELOG.md index f33de4a..6d57bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html ## [Unreleased] ### Added +- **heishamon** 0.7.0 — **the driver now states what an operator may command, so a host can render a control for it.** A `controls` entry in the `DRIVER` block names `set_heat_curve_offset`, labels it, and describes its one input: number, −3…+3, step 1, °C. FTW reads this and draws the row; before it, a hardware-verified control had no way to reach a person and its author was told to write a Home Assistant automation instead (srcfl/ftw#520, srcfl/ftw#738). The bounds are the defaults — `min_offset`/`max_offset` narrow them in config and `driver_command` clamps to whichever is tighter, so a narrowed site is safe; its UI just offers a step or two that land on the configured limit +- **The declared evidence is `write_ack`, not `readback`, because that is what the driver actually does.** `apply_offset` returns as soon as the publish succeeds. The pump does echo the value on `main/Z1_Heat_Request_Temp`, but it arrives on a later poll and nothing waits for it or compares it. Declaring `readback` would tell a host the setting was confirmed when all we know is that a message left, and it would suppress the "the pump does not confirm this setting" line the host shows for exactly this case. Making it a real readback means completing the command asynchronously against that echo — a larger change to the control path, not a metadata edit - **heishamon** 0.6.0 — **the pump now reports its electrical draw, and its outdoor temperature under the name every other heat pump uses.** FTW's heating view finds a heat pump by `hp_power_w` and finds nothing else if that metric is absent, so a working, hardware-verified driver had no page at all: not a chart, not a reading, not an entry. It emits `hp_power_w` from `main/Heat_Power_Consumption`, which `power_topic` overrides for a Heishamon build that names it differently, and only once that topic has arrived — a wrong name costs the power reading and nothing else. `hp_outside_temp_c` becomes `hp_outdoor_temp_c`, which is what nibe_local and myuplink report and what the view charts; the old name was read by nobody. Existing history under the old key stays where it is and stops growing - The Lua `DRIVER` version said 0.4.0 while the manifest said 0.5.0. Both now say 0.6.0. The drift was invisible because nothing compares them outside `make bump-driver` - The example config carried what reads like a real MQTT password. It is now a placeholder diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index f2ec3c2..76ef370 100644 --- a/SUPPORT_STATUS.md +++ b/SUPPORT_STATUS.md @@ -68,8 +68,8 @@ Catalog source is not proof that a target can install or run a driver. | growatt | 2.1.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | hardybarth | 1.0.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | hardybarth | 1.0.1 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | -| heishamon | 0.6.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | -| heishamon | 0.6.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | +| heishamon | 0.7.0 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | +| heishamon | 0.7.0 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | hello | 1.1.2 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | | hello | 1.1.2 | blixt-l1 | not_assessed | — | — | not_recorded | — | not_assessed | no | | huawei | 2.1.1 | ftw-core | not_assessed | — | — | not_recorded | — | not_assessed | no | diff --git a/devices.yaml b/devices.yaml index 3d4d04d..3951bf4 100644 --- a/devices.yaml +++ b/devices.yaml @@ -1015,7 +1015,7 @@ manufacturers: protocols: - protocol: mqtt driver: "heishamon" - version: "0.6.0" + version: "0.7.0" ders: [heatpump] control: true firmware_versions: "" diff --git a/drivers/lua/heishamon.lua b/drivers/lua/heishamon.lua index 70e6e27..265583e 100644 --- a/drivers/lua/heishamon.lua +++ b/drivers/lua/heishamon.lua @@ -38,7 +38,7 @@ DRIVER = { id = "heishamon", name = "Panasonic Aquarea (Heishamon)", manufacturer = "Panasonic", - version = "0.6.0", + version = "0.7.0", protocols = { "mqtt" }, capabilities = { "heatpump" }, description = "Panasonic Aquarea H/J/K/L/M-series heat pump via Heishamon MQTT bridge. Controls Zone 1 heat curve offset (Z1_Heat_Request_Temp) in range -3..+3 °C.", @@ -49,6 +49,25 @@ DRIVER = { verified_by = { "Rolf (Runneval)" }, verified_at = "2026-06-21", verification_notes = "Tested on WH-SXC09H3E8 (H-series) with Heishamon Large v4.1.6 on ESP32. MQTT via core-mosquitto on HA Green. Live metrics confirmed. Offset control verified via Z1_Heat_Request_Temp.", + -- What an operator may command, in terms a host UI can render without + -- knowing this driver. The bounds are the defaults below; min_offset and + -- max_offset can narrow them in config, and driver_command clamps to + -- whichever is tighter, so a narrowed site is safe — its UI just offers a + -- couple of steps that land on the configured limit. + -- + -- evidence is write_ack, not readback: apply_offset returns as soon as the + -- publish succeeds. The pump does echo the value back on + -- main/Z1_Heat_Request_Temp, but that arrives on a later poll and nothing + -- here waits for it or compares it. Claiming readback would tell a host + -- the setting was confirmed when all we know is that a message left. + controls = { + { + id = "set_heat_curve_offset", + label = "Heat curve offset", + evidence = "write_ack", + input = { type = "number", min = -3, max = 3, step = 1, unit = "°C" }, + }, + }, } PROTOCOL = "mqtt" diff --git a/index.yaml b/index.yaml index 7cd52c3..98fe260 100644 --- a/index.yaml +++ b/index.yaml @@ -257,13 +257,13 @@ drivers: size_bytes: 2193 sha256: "517e086af899dc511c86b00460cd8ce052a8d4973edee8b018e5824d4b550d6e" - name: "heishamon" - version: "0.6.0" + version: "0.7.0" tier: core protocol: mqtt ders: [heatpump] control: true - size_bytes: 8458 - sha256: "c5bb882d0dfba3f566cfe13a91e46fa361e02a64b1a249e133de2ec0366188ac" + size_bytes: 9405 + sha256: "f79611db52807b751cdcfa5ab4e43df42b22fdcc4018b5abbbadd69fc0711131" - name: "hello" version: "1.1.2" tier: community diff --git a/manifests/heishamon.yaml b/manifests/heishamon.yaml index e9c55b8..021ae90 100644 --- a/manifests/heishamon.yaml +++ b/manifests/heishamon.yaml @@ -1,5 +1,5 @@ name: "heishamon" -version: "0.6.0" +version: "0.7.0" tier: core author: "Sourceful Labs AB" protocol: mqtt @@ -14,9 +14,9 @@ tested_devices: notes: "Panasonic Aquarea H/J/K/L/M-series heat pump via Heishamon MQTT bridge. Controls Zone 1 heat curve offset (Z1_Heat_Request_Temp) in range -3..+3 °C." min_driver_version: "0.4.0" min_host_version: "2.0.0" -size_bytes: 8458 +size_bytes: 9405 dkb_id: "heishamon" -sha256: "c5bb882d0dfba3f566cfe13a91e46fa361e02a64b1a249e133de2ec0366188ac" +sha256: "f79611db52807b751cdcfa5ab4e43df42b22fdcc4018b5abbbadd69fc0711131" signature: "" bytecode_sha256: "" diff --git a/support-status.json b/support-status.json index f5bd6ed..37013b0 100644 --- a/support-status.json +++ b/support-status.json @@ -898,7 +898,7 @@ }, { "catalog_source": true, - "catalog_version": "0.6.0", + "catalog_version": "0.7.0", "driver_id": "heishamon", "package_id": null, "targets": { diff --git a/tests/test_ftw_repository.py b/tests/test_ftw_repository.py index a90549e..4feacf0 100644 --- a/tests/test_ftw_repository.py +++ b/tests/test_ftw_repository.py @@ -489,6 +489,25 @@ def test_a_control_driver_keeps_the_control_path_it_was_ported_with( assert artifact.rstrip().endswith("DRIVER = __sourceful_ftw_metadata") +def test_a_control_driver_keeps_its_declared_controls_in_the_signed_identity( + tmp_path: Path, keypair: tuple[str, str] +) -> None: + manifest, output = build(tmp_path, keypair) + heishamon = next( + driver for driver in manifest["drivers"] if driver["id"] == "heishamon" + ) + artifact = (output / Path(heishamon["url"]).name).read_text() + + source_marker = "-- heishamon.lua" + assert source_marker in artifact + signed_identity = artifact.split(source_marker, 1)[0] + assert "controls = {" in signed_identity + assert 'id = "set_heat_curve_offset"' in signed_identity + assert 'evidence = "write_ack"' in signed_identity + assert 'input = { type = "number", min = -3, max = 3' in signed_identity + assert artifact.rstrip().endswith("DRIVER = __sourceful_ftw_metadata") + + def test_a_driver_that_declares_read_only_keeps_its_write_guards( tmp_path: Path, keypair: tuple[str, str] ) -> None: diff --git a/tools/ftw_repository.py b/tools/ftw_repository.py index 76f2cff..060d0c6 100644 --- a/tools/ftw_repository.py +++ b/tools/ftw_repository.py @@ -232,7 +232,12 @@ def _lua_string_list(values: list[str]) -> str: return "{ " + ", ".join(_lua_string(value) for value in values) + " }" -def _ftw_artifact(raw: bytes, metadata: dict[str, Any], read_only: bool) -> bytes: +def _ftw_artifact( + raw: bytes, + metadata: dict[str, Any], + read_only: bool, + controls_body: str | None = None, +) -> bytes: """Add FTW metadata and the host-call polyfills older hosts lack. A driver the catalog marks control: true keeps its control functions. The @@ -271,6 +276,8 @@ def _ftw_artifact(raw: bytes, metadata: dict[str, Any], read_only: bool) -> byte fields.append(f" {name} = {_lua_string(value)},") if tested_models: fields.append(f" tested_models = {_lua_string_list(tested_models)},") + if controls_body is not None: + fields.append(" controls = {" + controls_body + "},") # A driver that declares itself read-only keeps the guards: those are # meters and telemetry gateways stating what they are. A driver the catalog @@ -481,7 +488,13 @@ def _load_channel(config_path: Path, repo_root: Path) -> list[dict[str, Any]]: if value: metadata[output_name] = value - artifact = _ftw_artifact(raw, metadata, read_only=not controls) + controls_body = _lua_named_table_body(body, "controls") if body else None + artifact = _ftw_artifact( + raw, + metadata, + read_only=not controls, + controls_body=controls_body, + ) if len(artifact) > MAX_DRIVER_BYTES: raise RepositoryError(f"{driver_id}: generated FTW artifact is too large")