From d9d29b145e93efc755fe55f50e9c52a18c0c6b00 Mon Sep 17 00:00:00 2001 From: huangkongsheng <59566679+huangkongsheng@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:20:06 +0800 Subject: [PATCH 1/2] fix(growatt): skip battery telemetry when no battery is connected PV-only Growatt still answers the battery registers with 0, so Overview rendered a phantom Battery node. Only emit battery telemetry when voltage/SoC/power indicate a real battery. Fixes srcfl/ftw#1669. Signed-off-by: huangkongsheng <59566679+huangkongsheng@users.noreply.github.com> --- drivers/lua/growatt.lua | 27 +++-- drivers/tests/test_growatt_pv_only_battery.py | 107 ++++++++++++++++++ manifests/growatt.yaml | 8 +- 3 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 drivers/tests/test_growatt_pv_only_battery.py diff --git a/drivers/lua/growatt.lua b/drivers/lua/growatt.lua index afc8789..5abd5ba 100644 --- a/drivers/lua/growatt.lua +++ b/drivers/lua/growatt.lua @@ -21,7 +21,7 @@ DRIVER = { id = "growatt", name = "Growatt hybrid inverter", manufacturer = "Growatt", - version = "2.1.2", + version = "2.1.3", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, read_only = true, @@ -181,14 +181,23 @@ function driver_poll() bat_temp = btemp_regs[1] * 0.1 end - host.emit("battery", { - w = bat_w, - v = bat_v, - soc = bat_soc, - temp_c = bat_temp, - }) - host.emit_metric("battery_dc_v", bat_v) - host.emit_metric("battery_temp_c", bat_temp) + -- A PV-only Growatt (no battery connected) still answers the battery + -- registers with 0, which made FTW render a phantom "Battery" node on + -- Overview even though no battery exists. A real battery reports a + -- terminal voltage and/or SoC above 0 even at rest, so treat all-zero + -- battery readings as "no battery present" and skip the emit. + -- see srcfl/ftw#1669 + local has_battery = bat_v > 0 or bat_soc > 0 or bat_charge_w > 0 or bat_discharge_w > 0 + if has_battery then + host.emit("battery", { + w = bat_w, + v = bat_v, + soc = bat_soc, + temp_c = bat_temp, + }) + host.emit_metric("battery_dc_v", bat_v) + host.emit_metric("battery_temp_c", bat_temp) + end -- ---- Meter ---- diff --git a/drivers/tests/test_growatt_pv_only_battery.py b/drivers/tests/test_growatt_pv_only_battery.py new file mode 100644 index 0000000..42a7fbe --- /dev/null +++ b/drivers/tests/test_growatt_pv_only_battery.py @@ -0,0 +1,107 @@ +"""A PV-only Growatt must not emit a phantom Battery stream. + +A Growatt with no battery connected still answers the battery Modbus +registers with 0. The driver used to emit a `battery` stream on every poll +regardless, which made FTW render a "Battery Growatt" node on Overview even +though no battery exists (srcfl/ftw#1669). The driver now only emits battery +telemetry when a battery is actually present. + +CI builds ./lua55 (see tools/build_luac.sh) and runs this through the host +mock, so the guard holds in the pipeline. pytest skips it locally when the +interpreter is absent. +""" + +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +LUA = ROOT / "lua55" +HARNESS = ROOT / "drivers" / "tests" / "lua_harness" +GROWATT = ROOT / "drivers" / "lua" / "growatt.lua" + +pytestmark = pytest.mark.skipif( + not LUA.exists(), reason="run make check to build ./lua55") + + +def _run(body: str) -> dict[str, str]: + script = f''' +package.path = "{HARNESS}/?.lua;" .. package.path +require("host_mock") +host.reset() + +{body} +''' + result = subprocess.run([str(LUA), "-e", script], + capture_output=True, text=True, cwd=ROOT) + assert result.returncode == 0, result.stdout + result.stderr + return dict(line.split(" ", 1) + for line in result.stdout.strip().splitlines() if " " in line) + + +def _poll(registers: dict[str, int]) -> dict[str, str]: + setup = "\n".join( + f"host._modbus_registers.input[{addr}] = {val}" + for addr, val in registers.items() + ) + return _run(f''' +{setup} +dofile("{GROWATT}") +driver_init({{host = '127.0.0.1', port = 502, unit_id = 1}}) +local ok, err = pcall(driver_poll) +if not ok then print("POLL_ERROR " .. tostring(err)) os.exit(1) end +local bat = host._emitted.battery +if bat and #bat > 0 then + print("BATTERY_EMITTED 1") + print("BAT_V " .. tostring(bat[1].v)) + print("BAT_SOC " .. tostring(bat[1].soc)) + print("BAT_W " .. tostring(bat[1].w)) +else + print("BATTERY_EMITTED 0") +end +''') + + +def test_pv_only_install_emits_no_battery_node(): + """A Growatt answering all-zero battery registers must not emit battery.""" + out = _poll({}) # every register unmapped -> the mock returns 0 + assert out["BATTERY_EMITTED"] == "0", ( + "PV-only Growatt emitted a battery stream; Overview would show a " + "phantom Battery node (srcfl/ftw#1669)." + ) + + +def test_hybrid_install_with_battery_emits_battery(): + """A real battery (voltage + SoC + charge power) must still be reported.""" + out = _poll({ + 1009: 0, 1010: 100, # charge power 10 W + 1011: 0, 1012: 0, # discharge power 0 W + 1013: 480, # terminal voltage 48.0 V + 1014: 50, # SoC 50 % + }) + assert out["BATTERY_EMITTED"] == "1", ( + "Hybrid Growatt with a battery present did not emit battery telemetry." + ) + assert float(out["BAT_V"]) == 48.0 + assert float(out["BAT_SOC"]) == 0.5 + assert float(out["BAT_W"]) == 10.0 # charge - discharge + + +def test_battery_at_rest_with_zero_soc_still_emits(): + """A present battery reads a terminal voltage even at 0 % SoC. + + Guards against an over-strict gate that would drop a real battery just + because its SoC register reads 0. + """ + out = _poll({ + 1009: 0, 1010: 0, + 1011: 0, 1012: 0, + 1013: 520, # 52.0 V present + 1014: 0, # SoC 0 % + }) + assert out["BATTERY_EMITTED"] == "1", ( + "A battery at 0 % SoC but with terminal voltage was dropped; the gate " + "must key off voltage/power, not SoC alone." + ) + assert float(out["BAT_V"]) == 52.0 diff --git a/manifests/growatt.yaml b/manifests/growatt.yaml index 2ec89bd..1e1121c 100644 --- a/manifests/growatt.yaml +++ b/manifests/growatt.yaml @@ -1,5 +1,5 @@ name: "growatt" -version: "2.1.2" +version: "2.1.3" tier: community author: "Sourceful Labs AB" protocol: modbus @@ -50,12 +50,12 @@ tested_devices: notes: "Community driver, untested" min_driver_version: "1.0.0" min_host_version: "2.0.0" -size_bytes: 9376 +size_bytes: 9923 dkb_id: "" -sha256: "1e9623cbf3453697f0fba429a5b8dab7d61cf9507587e616d0cd9de3cc3594c8" +sha256: "684c788e989c6f90b7a64fd79521f913104486f2c939bf13d4aaa2fbfddd9dad" signature: "" bytecode_sha256: "" bytecode_signature: "" bytecode_size: 0 -changelog: "" +changelog: "2.1.3: skip battery telemetry when no battery is connected (PV-only installs), fixes phantom Battery node on Overview (srcfl/ftw#1669)" From 36eaac70723278f2aee28d7c5fd2ac9d79cfac24 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Thu, 5 Nov 2026 14:52:30 +0100 Subject: [PATCH 2/2] chore(growatt): lower the invented-number baseline and regenerate for 2.1.3 The battery gate removes four invented numbers from growatt (20 to 16), so the ratchet needs its baseline lowered. index.yaml, devices.yaml and the support status are regenerated for the 2.1.3 manifest. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Fredrik Ahlgren --- SUPPORT_STATUS.md | 4 ++-- devices.yaml | 12 ++++++------ drivers/tests/invented-number-baseline.json | 2 +- index.yaml | 6 +++--- support-status.json | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/SUPPORT_STATUS.md b/SUPPORT_STATUS.md index 4fec4ef..f602598 100644 --- a/SUPPORT_STATUS.md +++ b/SUPPORT_STATUS.md @@ -72,8 +72,8 @@ Catalog source is not proof that a target can install or run a driver. | goe_http | 1.0.1 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | | goodwe | 2.1.2 | ftw-core | not_assessed | — | not_recorded | not_assessed | | goodwe | 2.1.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | -| growatt | 2.1.2 | ftw-core | not_assessed | — | not_recorded | not_assessed | -| growatt | 2.1.2 | blixt-l1 | not_assessed | — | not_recorded | not_assessed | +| growatt | 2.1.3 | ftw-core | not_assessed | — | not_recorded | not_assessed | +| growatt | 2.1.3 | 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.8.0 | ftw-core | not_assessed | — | not_recorded | not_assessed | diff --git a/devices.yaml b/devices.yaml index 2701aa6..7dfd711 100644 --- a/devices.yaml +++ b/devices.yaml @@ -533,7 +533,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.2" + version: "2.1.3" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -544,7 +544,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.2" + version: "2.1.3" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -555,7 +555,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.2" + version: "2.1.3" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -566,7 +566,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.2" + version: "2.1.3" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -577,7 +577,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.2" + version: "2.1.3" ders: [pv, battery, meter] control: false firmware_versions: "" @@ -588,7 +588,7 @@ manufacturers: protocols: - protocol: modbus driver: "growatt" - version: "2.1.2" + version: "2.1.3" ders: [pv, battery, meter] control: false firmware_versions: "" diff --git a/drivers/tests/invented-number-baseline.json b/drivers/tests/invented-number-baseline.json index 3cb282b..2c0de0f 100644 --- a/drivers/tests/invented-number-baseline.json +++ b/drivers/tests/invented-number-baseline.json @@ -31,7 +31,7 @@ "fronius": 11, "goe": 8, "goodwe": 24, - "growatt": 20, + "growatt": 16, "hello": 3, "janitza": 13, "keba": 8, diff --git a/index.yaml b/index.yaml index ebee215..1a99d9a 100644 --- a/index.yaml +++ b/index.yaml @@ -313,14 +313,14 @@ drivers: size_bytes: 9566 sha256: "148d36d25fa630dd11ab1f697c46dde60863dcd3a2a9944e78e322dba55bde49" - name: "growatt" - version: "2.1.2" + version: "2.1.3" tier: community protocol: modbus connectivity: local ders: [pv, battery, meter] control: false - size_bytes: 9376 - sha256: "1e9623cbf3453697f0fba429a5b8dab7d61cf9507587e616d0cd9de3cc3594c8" + size_bytes: 9923 + sha256: "684c788e989c6f90b7a64fd79521f913104486f2c939bf13d4aaa2fbfddd9dad" - name: "hardybarth" version: "1.0.1" tier: community diff --git a/support-status.json b/support-status.json index fd82e8f..309845d 100644 --- a/support-status.json +++ b/support-status.json @@ -716,7 +716,7 @@ }, { "catalog_source": true, - "catalog_version": "2.1.2", + "catalog_version": "2.1.3", "driver_id": "growatt", "targets": { "blixt-l1": {