Skip to content

Drivers: port Huawei SUN2000 hybrid inverter - #15

Merged
frahlg merged 1 commit into
masterfrom
port-huawei-sun2000-driver
Apr 15, 2026
Merged

frahlg merged 1 commit into
masterfrom
port-huawei-sun2000-driver

Conversation

@frahlg

@frahlg frahlg commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Ports the Huawei SUN2000 hybrid inverter driver from sourceful-hugin to the forty-two-watts v2.1 Lua host idiom.

  • Full lifecycle (init, poll, command, default_mode, cleanup) — all five are implemented.
  • Emits PV (two MPPTs), LUNA2000 battery, and built-in 3-phase meter. Supports forcible charge/discharge control over Modbus TCP.
  • DRIVER metadata block populated so it appears in the catalog; tested models: SUN2000L1, SUN2000-LUNA2000.

Notable porting decisions

  • host.log(msg) → host.log("info", msg) (v2.1 API).
  • host.decode_u32(hi,lo) → host.decode_u32_be(hi,lo); same for i32. Huawei uses big-endian word order so every multi-register decode became _be.
  • host.modbus_write_multiple → host.modbus_write_multi (v2.1 spelling).
  • Added a one-shot serial-number read from holding 30015 (10 regs ASCII) so host.set_sn anchors device_id to hardware identity. Hugin didn't do this.
  • Diagnostic scalars (MPPT V/A, inverter temp, battery DC V/A/temp, per-phase V/A/W, grid Hz) emitted via host.emit_metric into the long-format TS DB.
  • Site convention applied at the boundary: PV w is negated (generation is negative); meter w, per-phase w, and per-phase current are negated because Huawei reports positive = export.
  • Every host.modbus_read wrapped in pcall.
  • driver_default_mode and driver_cleanup both call set_self_consumption so a watchdog trip can't leave the battery stuck in forcible mode.

Test plan

  • cd go && go test -count=1 -run TestLua ./internal/drivers/ — passes.
  • Catalog load test (ad-hoc) confirms DRIVER block parses: id=huawei-sun2000, manufacturer=Huawei, protocols=[modbus], capabilities=[meter,pv,battery].
  • Lifecycle load test (ad-hoc) confirms all 5 functions dispatch cleanly on a real LuaDriver (make=Huawei captured; poll/command/default_mode run without Modbus capability since pcall swallows the no-capability errors).
  • cd go && go test ./... — full suite green.
  • Live test against a SUN2000-LUNA2000 (next field visit).

🤖 Generated with Claude Code

Ported from sourceful-hugin to the forty-two-watts v2.1 Lua host
idiom. Supports PV (two MPPTs), LUNA2000 battery, built-in 3-phase
meter, and forcible charge/discharge control over Modbus TCP.

- All lifecycle functions implemented (init, poll, command,
  default_mode, cleanup).
- DRIVER metadata block so it appears in the catalog.
- host.log(level, msg) and host.decode_u32_be / i32_be rewritten
  to v2.1 API (hugin used host.log(msg) + host.decode_u32).
- Reads ASCII serial number from holding 30015 once and anchors
  device identity via host.set_make + host.set_sn.
- Diagnostic scalars (MPPT V/A, inverter temp, battery DC V/A/temp,
  per-phase V/A/W, grid Hz) emitted via host.emit_metric.
- Site convention applied at the boundary: PV w negated (generation
  is negative), meter w + per-phase w + per-phase current negated
  (Huawei reports positive = export).
- All host.modbus_read calls wrapped in pcall.
- driver_default_mode and driver_cleanup revert to maximise
  self-consumption so a watchdog trip can't leave the battery
  stuck in forcible mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbb160b86f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread drivers/huawei.lua
Comment on lines +226 to +230
local ok_mw, mw_regs = pcall(host.modbus_read, 37113, 2, "holding")
local meter_w = 0
if ok_mw and mw_regs then
meter_w = host.decode_i32_be(mw_regs[1], mw_regs[2])
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail poll on meter read errors instead of publishing zeros

When host.modbus_read(37113, ...) fails, meter_w stays at 0 and the poll continues to host.emit("meter", { w = -meter_w, ... }), so a transport/register failure is reported as valid zero grid power rather than a failed sample. In this codebase, successful emits update freshness/health, so during Modbus outages this driver can keep telemetry “healthy” with fabricated zeros and drive incorrect control decisions (e.g., dispatch based on false no-import/no-export state) instead of tripping stale-data safeguards.

Useful? React with 👍 / 👎.

@frahlg
frahlg merged commit 09a8855 into master Apr 15, 2026
@frahlg
frahlg deleted the port-huawei-sun2000-driver branch April 15, 2026 08:39
github-actions Bot pushed a commit to erikarenhill/forty-two-watts that referenced this pull request Apr 15, 2026
## 1.0.0 (2026-04-15)

### Bug Fixes

* **ci:** switch semantic-release to conventionalcommits preset ([15290c7](15290c7))
* Lua driver Command() reading wrong field — Sungrow ignored targets ([9237156](9237156))

### Drivers

* add Eastron SDM630 Lua driver ([srcfl#18](https://github.com/erikarenhill/forty-two-watts/issues/18)) ([d5ad806](d5ad806))
* add Ferroamp Modbus driver (alt transport to ferroamp.lua) ([srcfl#31](https://github.com/erikarenhill/forty-two-watts/issues/31)) ([03b802c](03b802c))
* port Deye SUN-SG hybrid inverter to 42W v2.1 Lua host ([srcfl#29](https://github.com/erikarenhill/forty-two-watts/issues/29)) ([df8fbc0](df8fbc0))
* port Fronius GEN24 (SunSpec) to Lua ([srcfl#19](https://github.com/erikarenhill/forty-two-watts/issues/19)) ([c1fc875](c1fc875))
* port Fronius Smart Meter (SunSpec Modbus, read-only) ([srcfl#24](https://github.com/erikarenhill/forty-two-watts/issues/24)) ([575895c](575895c))
* port GoodWe hybrid inverter (ET-Plus / EH) to Lua v2.1 ([srcfl#28](https://github.com/erikarenhill/forty-two-watts/issues/28)) ([e43d2d9](e43d2d9))
* port Growatt hybrid inverter driver (read-only) ([srcfl#20](https://github.com/erikarenhill/forty-two-watts/issues/20)) ([92524ac](92524ac))
* port Huawei SUN2000 hybrid inverter ([srcfl#15](https://github.com/erikarenhill/forty-two-watts/issues/15)) ([09a8855](09a8855))
* port Kostal Plenticore / Piko IQ (Lua, read-only) ([srcfl#21](https://github.com/erikarenhill/forty-two-watts/issues/21)) ([bdeca96](bdeca96))
* port Pixii PowerShaper battery driver to v2.1 Lua host ([srcfl#22](https://github.com/erikarenhill/forty-two-watts/issues/22)) ([70a96d1](70a96d1))
* port SMA hybrid inverter Lua driver ([srcfl#23](https://github.com/erikarenhill/forty-two-watts/issues/23)) ([dd34555](dd34555))
* port Sofar HYD-ES/HYD-EP from hugin to Lua v2.1 ([srcfl#26](https://github.com/erikarenhill/forty-two-watts/issues/26)) ([14f6131](14f6131))
* port SolarEdge SunSpec inverter + meter to Lua (read-only) ([srcfl#30](https://github.com/erikarenhill/forty-two-watts/issues/30)) ([1007e63](1007e63))
* port Solis hybrid inverter ([srcfl#27](https://github.com/erikarenhill/forty-two-watts/issues/27)) ([98b2a50](98b2a50))
* port Victron Energy GX Modbus driver ([srcfl#25](https://github.com/erikarenhill/forty-two-watts/issues/25)) ([ad71db2](ad71db2))

### UI

* inline target on hover + driver card + collapsible model cards ([de88f43](de88f43))
* legend wrap + nice-tick y-axis + cleaner chart labels ([srcfl#33](https://github.com/erikarenhill/forty-two-watts/issues/33)) ([aeb1d1c](aeb1d1c))
* live version from API + no-cache headers on static assets ([7b8779b](7b8779b))

### Control loop

* fold live DerEV readings into the EV clamp ([srcfl#36](https://github.com/erikarenhill/forty-two-watts/issues/36)) ([5d57d68](5d57d68))

### MPC planner

* fall back to forecast when learned PV twin collapses ([srcfl#39](https://github.com/erikarenhill/forty-two-watts/issues/39)) ([f3062ac](f3062ac))

### Telemetry

* add DerEV type for EV charger readings ([srcfl#34](https://github.com/erikarenhill/forty-two-watts/issues/34)) ([65c9e2c](65c9e2c))

### TSDB

* long-format SQLite (14d) + Parquet rolloff for older ([c53c964](c53c964))

### Safety

* watchdog marks stale drivers offline + reverts to autonomous ([519196c](519196c))
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.0.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

github-actions Bot pushed a commit that referenced this pull request Apr 16, 2026
## [0.4.0](v0.3.0...v0.4.0) (2026-04-16)

### Features

* config/UI improvements — kWh display, secure EV password, planner tab ([#65](#65)) ([35ab03d](35ab03d))
* Easee Cloud driver + host.http_get/post for Lua drivers ([#56](#56)) ([4cdc942](4cdc942))
* EV charger config + credential masking in API responses ([#58](#58)) ([c22cb80](c22cb80))

### Bug Fixes

* 5 Go-side P1 bugs from Codex review ([#46](#46)) ([0cd2885](0cd2885))
* 5 Go-side P1 bugs from Codex review ([#47](#47)) ([4f2eaf6](4f2eaf6))
* address P2 review comments across control, MPC, drivers, and UI ([#64](#64)) ([fcafa88](fcafa88))
* **ci:** disable @semantic-release/github PR annotation features ([4020d46](4020d46)), closes [#32](#32) [#33](#33) [#34](#34) [#35](#35) [#36](#36) [#39](#39)
* **ci:** switch semantic-release to conventionalcommits preset ([7e0bb89](7e0bb89))
* **ci:** upgrade GitHub Actions to Node.js 24 (drop deprecated Node 20) ([4005bd8](4005bd8))
* Lua driver Command() reading wrong field — Sungrow ignored targets ([9237156](9237156))
* populate EV Charger tab from driver config when ev_charger is empty ([5e6b116](5e6b116))
* remove dead evSlider event listeners that crash app.js ([8ae76c7](8ae76c7))
* replace wonky Catmull-Rom spline with simple linear forecast ([abea431](abea431))
* show '...' instead of stale v0.1.0 while JS loads version ([dc65065](dc65065))
* **solaredge_pv:** read SunSpec scale factors every poll, not cached ([#38](#38)) ([26f8793](26f8793))

### Drivers

* add Eastron SDM630 Lua driver ([#18](#18)) ([d5ad806](d5ad806))
* add Ferroamp Modbus driver (alt transport to ferroamp.lua) ([#31](#31)) ([03b802c](03b802c))
* fix 9 P1 bugs flagged by Codex review ([#44](#44)) ([b20e485](b20e485))
* port Deye SUN-SG hybrid inverter to 42W v2.1 Lua host ([#29](#29)) ([df8fbc0](df8fbc0))
* port Fronius GEN24 (SunSpec) to Lua ([#19](#19)) ([c1fc875](c1fc875))
* port Fronius Smart Meter (SunSpec Modbus, read-only) ([#24](#24)) ([575895c](575895c))
* port GoodWe hybrid inverter (ET-Plus / EH) to Lua v2.1 ([#28](#28)) ([e43d2d9](e43d2d9))
* port Growatt hybrid inverter driver (read-only) ([#20](#20)) ([92524ac](92524ac))
* port Huawei SUN2000 hybrid inverter ([#15](#15)) ([09a8855](09a8855))
* port Kostal Plenticore / Piko IQ (Lua, read-only) ([#21](#21)) ([bdeca96](bdeca96))
* port Pixii PowerShaper battery driver to v2.1 Lua host ([#22](#22)) ([70a96d1](70a96d1))
* port SMA hybrid inverter Lua driver ([#23](#23)) ([dd34555](dd34555))
* port Sofar HYD-ES/HYD-EP from hugin to Lua v2.1 ([#26](#26)) ([14f6131](14f6131))
* port SolarEdge SunSpec inverter + meter to Lua (read-only) ([#30](#30)) ([1007e63](1007e63))
* port Solis hybrid inverter ([#27](#27)) ([98b2a50](98b2a50))
* port Victron Energy GX Modbus driver ([#25](#25)) ([ad71db2](ad71db2))

### UI

* add status bar with driver health indicators ([b048d60](b048d60))
* EV status card + Easee control commands ([#59](#59)) ([b03749a](b03749a))
* fix summary cards grid for 7 cards + raise side-by-side breakpoint ([6e19973](6e19973))
* inline target on hover + driver card + collapsible model cards ([de88f43](de88f43))
* legend wrap + nice-tick y-axis + cleaner chart labels ([#33](#33)) ([aeb1d1c](aeb1d1c))
* remove manual EV charging slider ([063174c](063174c))
* show mode band in plan chart + grid target on status card ([877e0bd](877e0bd))
* smooth Catmull-Rom spline for forecast + 15min forecast zone ([dba51a5](dba51a5))

### Control loop

* fold live DerEV readings into the EV clamp ([#36](#36)) ([5d57d68](5d57d68))
* slew-rate anchors on actual battery power, not stale command ([#41](#41)) ([4f73f19](4f73f19))

### MPC planner

* fall back to forecast when learned PV twin collapses ([#39](#39)) ([f3062ac](f3062ac))
* log optimize params + ems_mode per action for plan chart ([9e8c14b](9e8c14b))
* value SoC at import−export spread in self-consumption modes ([#40](#40)) ([a90d525](a90d525))

### Telemetry

* add DerEV type for EV charger readings ([#34](#34)) ([65c9e2c](65c9e2c))

### TSDB

* long-format SQLite (14d) + Parquet rolloff for older ([c53c964](c53c964))

### Safety

* watchdog marks stale drivers offline + reverts to autonomous ([519196c](519196c))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant