diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ec56f..b358dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## heishamon 0.8.0 + +Read `hp_power_w` from `main/Heat_Power_Consumption` (TOP16), with a +`power_topic` override. This is the heat-mode input, not total electrical +input in cooling or DHW mode. The new reading remains unverified on +hardware. Invalid negative or non-finite power values clear the reading; +newer pumps can send −200 on this legacy topic. Power expires after +60 seconds without its own update, even if other topics keep arriving. + +Use `hp_outdoor_temp_c` for outdoor temperature, matching the other +heat-pump drivers and FTW's Heating view. Prior history keeps its old key. + ## heishamon 0.7.0 Declare the existing `set_heat_curve_offset` control: −3…+3 °C, in steps @@ -315,7 +327,6 @@ Driver versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html comments. No behaviour change. ### Added - - **acuvim** 0.4.1 — Accuenergy Acuvim II three-phase revenue-grade meter, migrated from the Blixt L1 driver source (POI meter on Blixt SvK sites). One bundled FC03 float32 block + a bounded probe of the diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index 450e4e9..40e329d 100644 --- a/SUPPORT_STATUS.md +++ b/SUPPORT_STATUS.md @@ -76,8 +76,8 @@ Catalog source is not proof that a target can install or run a driver. | growatt | 2.1.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | | hardybarth | 1.0.1 | ftw-core | not_assessed | — | not_recorded | not_assessed | | hardybarth | 1.0.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | -| heishamon | 0.7.0 | ftw-core | not_assessed | — | not_recorded | not_assessed | -| heishamon | 0.7.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | +| heishamon | 0.8.0 | ftw-core | not_assessed | — | not_recorded | not_assessed | +| heishamon | 0.8.0 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | | hello | 1.1.2 | ftw-core | not_assessed | — | not_recorded | not_assessed | | hello | 1.1.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | | huawei | 2.1.3 | ftw-core | not_assessed | — | not_recorded | not_assessed | diff --git a/devices.yaml b/devices.yaml index 9258bb2..592c11c 100644 --- a/devices.yaml +++ b/devices.yaml @@ -1008,7 +1008,7 @@ manufacturers: protocols: - protocol: mqtt driver: "heishamon" - version: "0.7.0" + version: "0.8.0" ders: [heatpump] control: true firmware_versions: "" diff --git a/drivers/lua/heishamon.lua b/drivers/lua/heishamon.lua index 1ba2263..a0f466e 100644 --- a/drivers/lua/heishamon.lua +++ b/drivers/lua/heishamon.lua @@ -24,6 +24,14 @@ -- min_offset: -3 -- max_offset: 3 -- safe_offset: 0 +-- power_topic: Heat_Power_Consumption +-- +-- power_topic names a main/ topic carrying electrical power in W. +-- The default is Heat_Power_Consumption (TOP16), the heat-mode input. +-- It is not total input in cooling or DHW mode. Pumps with an extra data +-- block can send invalid negative values on this legacy topic; omit them. +-- This new reading has not been checked on hardware. +-- Source: https://github.com/heishamon/HeishaMon/blob/main/MQTT-Topics.md DRIVER = { host_api_min = 1, @@ -31,7 +39,7 @@ DRIVER = { id = "heishamon", name = "Panasonic Aquarea (Heishamon)", manufacturer = "Panasonic", - version = "0.7.0", + version = "0.8.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.", @@ -41,7 +49,7 @@ DRIVER = { verification_status = "experimental", 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.", + verification_notes = "Existing metrics and offset control tested on WH-SXC09H3E8 (H-series) with Heishamon Large v4.1.6 on ESP32 on 2026-06-21. The new hp_power_w reading is not hardware-verified; its default topic reports heat-mode input, not total input in cooling or DHW mode.", -- 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 @@ -71,7 +79,9 @@ local outlet_temp = nil local inlet_temp = nil local target_temp = nil local z1_offset = nil +local power_w = nil local last_msg_ts = 0 +local last_power_ts = 0 local STALE_AFTER_MS = 60000 -- Config (overridable via config.yaml) @@ -79,6 +89,7 @@ local base_topic = "panasonic_heat_pump" local min_offset = -3 local max_offset = 3 local safe_offset = 0 +local power_topic = "Heat_Power_Consumption" ---------------------------------------------------------------------------- -- Lifecycle @@ -92,6 +103,7 @@ function driver_init(config) if config.min_offset then min_offset = tonumber(config.min_offset) or -3 end if config.max_offset then max_offset = tonumber(config.max_offset) or 3 end if config.safe_offset then safe_offset = tonumber(config.safe_offset) or 0 end + if config.power_topic then power_topic = config.power_topic end end -- Subscribe broadly to all Heishamon topics @@ -104,7 +116,8 @@ function driver_init(config) host.log("info", "Heishamon: initialized, base_topic=" .. base_topic .. " offset_range=[" .. min_offset .. ".." .. max_offset .. "]" - .. " safe_offset=" .. safe_offset) + .. " safe_offset=" .. safe_offset + .. " power_topic=" .. power_topic) end function driver_poll() @@ -114,7 +127,18 @@ function driver_poll() for _, msg in ipairs(messages) do local val = tonumber(msg.payload) - if val ~= nil then + if msg.topic == base_topic .. "/main/" .. power_topic then + -- Heat consumption cannot be negative. Some newer pumps use + -- -200 as an invalid value on the legacy power topic. + if val ~= nil and val == val and val >= 0 and val < math.huge then + power_w = val + last_power_ts = now + last_msg_ts = now + else + power_w = nil + last_power_ts = 0 + end + elseif val ~= nil then if msg.topic == base_topic .. "/main/Outside_Temp" then outside_temp = val last_msg_ts = now @@ -143,14 +167,24 @@ function driver_poll() inlet_temp = nil target_temp = nil z1_offset = nil + power_w = nil + last_power_ts = 0 + elseif last_power_ts > 0 and (now - last_power_ts) > STALE_AFTER_MS then + host.log("warn", "Heishamon: no power reading for " + .. tostring(STALE_AFTER_MS) .. " ms — power stale") + power_w = nil + last_power_ts = 0 end - -- Emit metrics - if outside_temp ~= nil then host.emit_metric("hp_outside_temp_c", outside_temp, "°C") end + -- Emit metrics. Names are the ones FTW's heating view reads: it finds a + -- heat pump by hp_power_w and charts the outdoor temperature under + -- hp_outdoor_temp_c, the same names nibe_local and myuplink report. + if outside_temp ~= nil then host.emit_metric("hp_outdoor_temp_c", outside_temp, "°C") end if outlet_temp ~= nil then host.emit_metric("hp_outlet_temp_c", outlet_temp, "°C") end if inlet_temp ~= nil then host.emit_metric("hp_inlet_temp_c", inlet_temp, "°C") end if target_temp ~= nil then host.emit_metric("hp_target_temp_c", target_temp, "°C") end if z1_offset ~= nil then host.emit_metric("hp_z1_heat_offset", z1_offset, "°C") end + if power_w ~= nil then host.emit_metric("hp_power_w", power_w, "W") end return 5000 end @@ -205,4 +239,6 @@ function driver_cleanup() inlet_temp = nil target_temp = nil z1_offset = nil + power_w = nil + last_power_ts = 0 end diff --git a/drivers/tests/lua_harness/test_heishamon_power_freshness.lua b/drivers/tests/lua_harness/test_heishamon_power_freshness.lua new file mode 100644 index 0000000..6591f0b --- /dev/null +++ b/drivers/tests/lua_harness/test_heishamon_power_freshness.lua @@ -0,0 +1,60 @@ +dofile("drivers/tests/lua_harness/host_mock.lua") +host.reset() +dofile("drivers/lua/heishamon.lua") +driver_init({}) + +host._mqtt_buffer = { + { + topic = "panasonic_heat_pump/main/Heat_Power_Consumption", + payload = "4200", + }, + { + topic = "panasonic_heat_pump/main/Outside_Temp", + payload = "5", + }, +} +driver_poll() + +local fresh_power = host._metrics.hp_power_w +if fresh_power == nil or fresh_power.value ~= 4200 then + error("fresh Heishamon power reading was not emitted") +end + +host._metrics = {} +host._millis_counter = 60100 +host._mqtt_buffer = {{ + topic = "panasonic_heat_pump/main/Outside_Temp", + payload = "6", +}} +driver_poll() + +if host._metrics.hp_power_w ~= nil then + error("stale Heishamon power survived an unrelated fresh topic") +end + +local fresh_outdoor = host._metrics.hp_outdoor_temp_c +if fresh_outdoor == nil or fresh_outdoor.value ~= 6 then + error("fresh unrelated Heishamon telemetry was dropped with stale power") +end + +-- Newer heat pumps can send -200 on the old power topic. Invalid values +-- must clear the previous power reading while other measurements survive. +for _, invalid in ipairs({"-200", "nan", "1e99"}) do + host._metrics = {} + host._mqtt_buffer = {{topic="panasonic_heat_pump/main/Heat_Power_Consumption", payload="4200"}} + driver_poll() + assert(host._metrics.hp_power_w.value == 4200, "valid power did not recover") + host._metrics = {} + host._mqtt_buffer = { + {topic="panasonic_heat_pump/main/Heat_Power_Consumption", payload=invalid}, + {topic="panasonic_heat_pump/main/Outside_Temp", payload="7"}, + } + driver_poll() + assert(host._metrics.hp_power_w == nil, "invalid power was emitted: " .. invalid) + assert(host._metrics.hp_outdoor_temp_c.value == 7, "invalid power dropped outdoor temperature") +end + +host._metrics = {} +host._mqtt_buffer = {{topic="panasonic_heat_pump/main/Heat_Power_Consumption", payload="0"}} +driver_poll() +assert(host._metrics.hp_power_w.value == 0, "measured zero power was dropped") diff --git a/drivers/tests/test_stale_reads.py b/drivers/tests/test_stale_reads.py index 5727056..f8edfb9 100644 --- a/drivers/tests/test_stale_reads.py +++ b/drivers/tests/test_stale_reads.py @@ -28,6 +28,13 @@ def test_mqtt_driver_does_not_emit_cached_data_on_idle_poll(driver_name): run_lua("drivers/tests/lua_harness/test_mqtt_stale.lua", driver_name) +def test_heishamon_power_freshness_is_not_extended_by_other_topics(): + run_lua( + "drivers/tests/lua_harness/test_heishamon_power_freshness.lua", + "heishamon", + ) + + @pytest.mark.parametrize("app", ["ProEM", "MiniPMG3", "Plus2PM"]) def test_shelly_poll_emits_nothing_when_all_reads_fail(app): run_lua("drivers/tests/lua_harness/test_shelly_failures.lua", app) diff --git a/index.yaml b/index.yaml index 75e4ce7..3dff6c9 100644 --- a/index.yaml +++ b/index.yaml @@ -331,15 +331,15 @@ drivers: size_bytes: 2193 sha256: "517e086af899dc511c86b00460cd8ce052a8d4973edee8b018e5824d4b550d6e" - name: "heishamon" - version: "0.7.0" + version: "0.8.0" tier: core protocol: mqtt connectivity: local setup: [bridge] ders: [heatpump] control: true - size_bytes: 7932 - sha256: "aed6707a656a7e4e17c5edd00e8d081ed485b580d078173c13bdad75d9ffcaac" + size_bytes: 9855 + sha256: "5c322f14e58a2b9aad900813bea9438127249ebae2e76a4208cd74536703d7b6" - name: "hello" version: "1.1.2" tier: community diff --git a/manifests/heishamon.yaml b/manifests/heishamon.yaml index 7403568..6ac59b8 100644 --- a/manifests/heishamon.yaml +++ b/manifests/heishamon.yaml @@ -1,5 +1,5 @@ name: "heishamon" -version: "0.7.0" +version: "0.8.0" tier: core author: "Sourceful Labs AB" protocol: mqtt @@ -16,9 +16,15 @@ 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: 7932 +size_bytes: 9855 dkb_id: "heishamon" -sha256: "aed6707a656a7e4e17c5edd00e8d081ed485b580d078173c13bdad75d9ffcaac" +sha256: "5c322f14e58a2b9aad900813bea9438127249ebae2e76a4208cd74536703d7b6" signature: "" bytecode_sha256: "" + +upstream_docs: + - url: "https://raw.githubusercontent.com/heishamon/HeishaMon/main/MQTT-Topics.md" + title: "HeishaMon MQTT topics" + kind: api_docs + url_stability: stable diff --git a/support-status.json b/support-status.json index 4f3ea99..5179564 100644 --- a/support-status.json +++ b/support-status.json @@ -758,7 +758,7 @@ }, { "catalog_source": true, - "catalog_version": "0.7.0", + "catalog_version": "0.8.0", "driver_id": "heishamon", "targets": { "blixt-l1": {