-- GoodWe ET (GW8KN-ET) Hybrid Inverter + HK3000 Meter Driver -- Emits: PV, Meter, Battery (if present) -- Protocol: Modbus TCP (port 502) -- -- Register type: HOLDING (FC 0x03) -- Byte/word order: single U16/I16 registers (no multi-register pairs for power) -- Tested: GW8KN-ET with HK3000 smart meter -- READ-ONLY: control not implemented. -- -- IMPORTANT: Modbus TCP over WiFi is DISABLED by default on the GoodWe -- LAN+WiFi dongle. To enable it: -- -- 1. Open the SolarGo app and connect to the inverter. -- 2. Go to "Parameters". -- 3. Long-press (>5 seconds) on the firmware version line. -- 4. A hidden DEBUG/Expert screen appears. -- 5. Set register 47040 from 0 to 1. -- 6. Modbus TCP on port 502 is now accessible over WiFi. -- -- Without this step the inverter will accept TCP connections but never -- respond to Modbus frames. The setting persists across reboots. DRIVER = { id = "goodwe-et", name = "GoodWe ET inverter", manufacturer = "GoodWe", version = "1.0.0", protocols = { "modbus" }, capabilities = { "meter", "pv", "battery" }, description = "GoodWe GW*KN-ET series with HK3000 meter via Modbus TCP.", homepage = "https://en.goodwe.com", authors = { "forty-two-watts contributors" }, tested_models = { "GW8KN-ET" }, verification_status = "beta", verification_notes = "Verified against live GW8KN-ET with HK3000 meter. Register map differs from ET-Plus/EH.", connection_defaults = { port = 502, unit_id = 247, }, } PROTOCOL = "modbus" ---------------------------------------------------------------------------- -- Initialization ---------------------------------------------------------------------------- function driver_init(config) host.set_make("GoodWe") end ---------------------------------------------------------------------------- -- Telemetry polling ---------------------------------------------------------------------------- function driver_poll() ------------------------------------------------------------------------ -- PV ------------------------------------------------------------------------ -- PV1 voltage: 35107, U16 × 0.1 V -- PV1 current: 35108, U16 × 0.1 A -- PV1 power: 35110, I16 W (wraps to -1/-2 when idle) local ok_pv, pv_regs = pcall(host.modbus_read, 35107, 4, "holding") local mppt1_v, mppt1_a, pv_w = 0, 0, 0 if ok_pv and pv_regs then mppt1_v = pv_regs[1] * 0.1 mppt1_a = pv_regs[2] * 0.1 local raw_pv = host.decode_i16(pv_regs[4]) pv_w = (raw_pv > 0) and raw_pv or 0 end -- Total inverter AC output: 35138, I16 W (wraps to -1/-2 when idle) local ok_tot, tot_regs = pcall(host.modbus_read, 35138, 1, "holding") local inverter_w = 0 if ok_tot and tot_regs then local raw_inv = host.decode_i16(tot_regs[1]) inverter_w = (raw_inv > 0) and raw_inv or 0 end -- Grid frequency: 35123, U16 × 0.01 Hz local ok_hz, hz_regs = pcall(host.modbus_read, 35123, 1, "holding") local hz = 0 if ok_hz and hz_regs then hz = hz_regs[1] * 0.01 end host.emit("pv", { w = -inverter_w, -- negative = generation (site convention) mppt1_v = mppt1_v, mppt1_a = mppt1_a, }) host.emit_metric("pv_mppt1_v", mppt1_v) host.emit_metric("pv_mppt1_a", mppt1_a) host.emit_metric("pv_string1_w", pv_w) host.emit_metric("grid_hz", hz) ------------------------------------------------------------------------ -- Meter (grid connection point via HK3000) ------------------------------------------------------------------------ -- Grid meter total: 35140, I16 W -- GoodWe convention: positive = export, negative = import. -- Site convention: positive = import, negative = export. Negate. local ok_mw, mw_regs = pcall(host.modbus_read, 35140, 1, "holding") local meter_w = 0 if ok_mw and mw_regs then meter_w = -host.decode_i16(mw_regs[1]) end -- Meter per-phase voltage: 35145, 35151, 35157 (U16 x0.1 V) local ok_m1, m1_regs = pcall(host.modbus_read, 35145, 1, "holding") local l1_v = 0 if ok_m1 and m1_regs then l1_v = m1_regs[1] * 0.1 end local ok_m2, m2_regs = pcall(host.modbus_read, 35151, 1, "holding") local l2_v = 0 if ok_m2 and m2_regs then l2_v = m2_regs[1] * 0.1 end local ok_m3, m3_regs = pcall(host.modbus_read, 35157, 1, "holding") local l3_v = 0 if ok_m3 and m3_regs then l3_v = m3_regs[1] * 0.1 end -- Per-phase grid power: computed from inverter AC output minus house load. -- The HK3000 exposes per-phase LOAD at 35164/35166/35168 (U16, W) and the -- inverter reports per-phase AC output at 35125/35130/35135 (I16, W). -- Grid per phase = inverter_phase - load_phase (positive = export). -- We negate to match site convention (positive = import). local ok_load, load_regs = pcall(host.modbus_read, 35164, 5, "holding") local load_l1, load_l2, load_l3 = 0, 0, 0 if ok_load and load_regs then load_l1 = load_regs[1] -- 35164 load_l2 = load_regs[3] -- 35166 load_l3 = load_regs[5] -- 35168 end -- Inverter per-phase AC output: 35125/35130/35135 (I16, W) local ok_inv, inv_regs = pcall(host.modbus_read, 35125, 11, "holding") local inv_l1, inv_l2, inv_l3 = 0, 0, 0 if ok_inv and inv_regs then inv_l1 = host.decode_i16(inv_regs[1]) -- 35125 inv_l2 = host.decode_i16(inv_regs[6]) -- 35130 inv_l3 = host.decode_i16(inv_regs[11]) -- 35135 end -- Grid = -(inverter - load) in site convention (positive = import) local l1_w = -(inv_l1 - load_l1) local l2_w = -(inv_l2 - load_l2) local l3_w = -(inv_l3 - load_l3) -- Current derived from power/voltage for sub-amp resolution. local l1_a, l2_a, l3_a = 0, 0, 0 if l1_v > 0 then l1_a = l1_w / l1_v end if l2_v > 0 then l2_a = l2_w / l2_v end if l3_v > 0 then l3_a = l3_w / l3_v end -- Energy counters: import 35195-35196, export 35198-35199 (U32 BE × 0.1 kWh) local ok_imp, imp_regs = pcall(host.modbus_read, 35195, 2, "holding") local import_wh = 0 if ok_imp and imp_regs then import_wh = host.decode_u32_be(imp_regs[1], imp_regs[2]) * 0.1 * 1000 end local ok_exp, exp_regs = pcall(host.modbus_read, 35198, 2, "holding") local export_wh = 0 if ok_exp and exp_regs then export_wh = host.decode_u32_be(exp_regs[1], exp_regs[2]) * 0.1 * 1000 end host.emit("meter", { w = meter_w, l1_w = l1_w, l2_w = l2_w, l3_w = l3_w, l1_v = l1_v, l2_v = l2_v, l3_v = l3_v, l1_a = l1_a, l2_a = l2_a, l3_a = l3_a, hz = hz, import_wh = import_wh, export_wh = export_wh, }) host.emit_metric("meter_l1_v", l1_v) host.emit_metric("meter_l2_v", l2_v) host.emit_metric("meter_l3_v", l3_v) host.emit_metric("meter_l1_a", l1_a) host.emit_metric("meter_l2_a", l2_a) host.emit_metric("meter_l3_a", l3_a) ------------------------------------------------------------------------ -- Battery (only emitted if a battery is connected) ------------------------------------------------------------------------ -- Battery SoC: 35182, U16, percent local ok_bsoc, bsoc_regs = pcall(host.modbus_read, 35182, 1, "holding") local bat_soc = 0 if ok_bsoc and bsoc_regs then bat_soc = bsoc_regs[1] end -- Battery power: 35180, I16, W (positive=charge, negative=discharge) local ok_bw, bw_regs = pcall(host.modbus_read, 35180, 1, "holding") local bat_w = 0 if ok_bw and bw_regs then bat_w = host.decode_i16(bw_regs[1]) end -- Only emit battery telemetry if a battery is actually present. -- A disconnected BMS reports SoC=0 and power=0 permanently. if bat_soc > 0 or bat_w ~= 0 then -- Battery voltage: 35178, U16 × 0.1 V local ok_bv, bv_regs = pcall(host.modbus_read, 35178, 1, "holding") local bat_v = 0 if ok_bv and bv_regs then bat_v = bv_regs[1] * 0.1 end -- Battery temperature: 35183, I16 × 0.1 C local ok_bt, bt_regs = pcall(host.modbus_read, 35183, 1, "holding") local bat_temp = 0 if ok_bt and bt_regs then bat_temp = host.decode_i16(bt_regs[1]) * 0.1 end host.emit("battery", { w = bat_w, soc = bat_soc / 100, -- percent to 0-1 fraction v = bat_v, temp_c = bat_temp, }) host.emit_metric("battery_dc_v", bat_v) host.emit_metric("battery_temp_c", bat_temp) end return 5000 end ---------------------------------------------------------------------------- -- Control (read-only driver — not implemented) ---------------------------------------------------------------------------- function driver_command(action, power_w, cmd) if action == "init" then return true end host.log("warn", "GoodWe ET: control not implemented (action=" .. tostring(action) .. ")") return false end function driver_default_mode() -- Read-only driver: nothing to revert. end function driver_cleanup() -- Read-only driver: nothing to clean up. end