Skip to content

feat(foxess_h3_smart): Fox ESS 1K5 / H3-Smart register map driver - #69

Merged
frahlg merged 2 commits into
mainfrom
add-foxess-h3-smart
Aug 4, 2026
Merged

feat(foxess_h3_smart): Fox ESS 1K5 / H3-Smart register map driver#69
frahlg merged 2 commits into
mainfrom
add-foxess-h3-smart

Conversation

@Leitet

@Leitet Leitet commented Aug 3, 2026

Copy link
Copy Markdown
Member

Closes #68.

New read-only driver foxess_h3_smart 0.1.0 for Fox ESS inverters on the H3-Smart register map: the 1K5-HI three-phase hybrid series and the H3-Smart family that shares the map. The existing foxess driver (H1/H3 11000-range map) gets only silence from this hardware — it answers unknown registers with a timeout, not a Modbus exception.

Device and evidence

  • Hardware tested: Fox ESS 1K5-HI-10-V1, three-phase, two PV strings, one battery string, reached over Modbus-TCP :502 unit 247 on a LAN address. Firmware version registers (36001..36003, raw): master 324, slave 256, manager 294.
  • Register source: nathanmarlor/foxess_modbus Inv.H3_SMART profile (their issue #807 covers the 1K5 family). Recorded in the manifest's upstream_docs.
  • Values compared on hardware (mid-generation capture):
    • PV strings 3121 W + 3475 W; V×A cross-check agrees within 0.5% (275.2 V × 11.30 A ≈ 3110 W; 304.3 V × 11.40 A ≈ 3469 W).
    • Energy balance: load 997 W + battery charge 3727 W − PV 6596 W = −1872 W against grid CT −2004.5 W (site convention) — the gap is inverter losses.
    • Battery: SoC 93%, 35.4 °C, 183.9 V; current register follows the power register's sign (−15.00 A at −2803 W while charging).
    • Lifetime counters at 0.01 kWh scale are plausible and internally consistent (solar 12.8 MWh, battery charge/discharge ratio ~91%).
    • Not yet compared against the FoxESS cloud/app.
  • Offline / stale behavior: a block that fails three consecutive reads is left alone until restart, so a permanently silent register cannot keep failing every poll while the driver reports telemetry (the absent-register ratchet passes with no baseline entry). A failed block drops its emit — no fabricated zeros. Reconnect behavior not hardware-tested.
  • Known limits: emits one aggregated battery (total power + BMS-1 SoC); two-battery-string setups untested. H3-Smart family untested (same map per foxess_modbus). Per-phase CT powers exist in the map but are not emitted.

Tests

drivers/tests/test_foxess_h3_smart.py replays the hardware capture through the Lua harness and holds emitted signs and scales to the site convention, plus block-failure, SoC-omission and identity-retry behavior. The harness gained host.set_model and host.decode_string — both in the host profile, neither in the mock until a driver called them.

make check passes (3563 tests); ftw_repository.py check-versions against the published beta manifest answers added: [foxess_h3_smart], changed: [].

🤖 Generated with Claude Code

The existing foxess driver speaks the H1/H3 11000-range map; a 1K5
answers none of it, silently. New read-only driver for the H3-Smart
map (1K5-HI series, H3-Smart family), register map from
nathanmarlor/foxess_modbus, telemetry validated on 1K5-HI-10-V1
hardware.

Harness gains host.set_model and host.decode_string, both in the host
profile but missing from the mock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Leitet <johan@sourceful-labs.com>

frahlg commented Aug 3, 2026

Copy link
Copy Markdown
Member

Automated maintenance pass: not merging this one — real, reproducible build failure, not a flake.

What's good: the driver itself reads carefully. Sign conversion matches the site convention (PV negative, battery positive-on-charge via the vendor's negative-charging flip, meter positive-on-import), every register read goes through the bounded read() helper (gives up after 3 failures, no fabricated zeros on a dropped block), the BMS singles are read one-at-a-time as the hardware requires, and test_foxess_h3_smart.py replays an actual hardware capture with PV/battery/meter cross-checks that hold up. upstream_docs is recorded. CHANGELOG, manifest, index.yaml, devices.yaml and support-status.json are all consistently updated for a new 0.1.0 driver.

Why CI is red: the packages check fails, and it's not incidental — I checked out this exact commit locally and reproduced it directly:

$ uv run --no-sync python tools/build_candidate.py --id foxess_h3_smart --output-dir /tmp/x
blixt-l1: Lua source must declare DRIVER_MANIFEST

packages/v1/foxess_h3_smart/package-source.json declares compatibility with both ftw-core and blixt-l1, copying the same drivers/lua/foxess_h3_smart.lua for each target — the same pattern sdm630 uses. But sdm630.lua declares both a DRIVER table (for ftw-core) and a separate DRIVER_MANIFEST table (for blixt-l1, per tools/driver_package.py's _validate_lua_source_for_target), while foxess_h3_smart.lua only declares DRIVER. Confirmed this isn't a pre-existing or flaky failure: the validate workflow run on main at this PR's exact base commit (e9f2645) is green, and running the full packages/v1/* build loop locally in order reproduces the same failure only at foxess_h3_smart, with every other package (esphome-dsmr, pixii, sdm630, sungrow) succeeding.

Two ways to fix it, either is fine:

  1. Add a DRIVER_MANIFEST table to drivers/lua/foxess_h3_smart.lua (see sdm630.lua:33 for the shape: name, version, role, requires, options, provides.live/provides.static).
  2. Drop the blixt-l1 entry from packages/v1/foxess_h3_smart/package-source.json's compatibility and artifact_inputs if Blixt L1 support for this driver isn't actually intended yet, and update support-status.json's blixt-l1 block accordingly.

Everything else about this PR looks ready to merge once that's resolved.


Generated by Claude Code

@miravoss26 miravoss26 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.

The driver reads well. Register map documented against nathanmarlor/foxess_modbus and validated on 1K5-HI-10-V1, signs normalised to site convention (pv negative, battery/meter flipped from the vendor's), the BMS block read single-register as its hardware demands, and the give-up-after-3 guard is a good call given this device answers an unknown register with silence rather than a Modbus exception. Read-only, driver_default_mode commands nothing. Correctness-wise I'd take it.

CI is the blocker: the packages job is red (the other five — drivers, signed-channel, public-boundary, dco, secret-scan — pass). It dies in the blixt-l1 candidate build with Lua source must declare DRIVER_MANIFEST. The last candidate the log printed before the failure was esphome-dsmr@1.0.2 for ftw-core, and the build is fail-fast, so I can't tell from the log alone which source tripped it. The new foxess_h3_smart.lua declares DRIVER = {...}, not DRIVER_MANIFEST — worth checking whether the blixt-l1 target wants that symbol on the new driver, or whether this is a pre-existing esphome-dsmr/blixt-l1 break the PR just inherits (the diff doesn't touch esphome-dsmr).

Needs a human on that packages failure before it lands. Not in my auto-merge allowlist regardless, so treat this as a flag, not a block.

The package declares both targets, and blixt-l1 requires a
DRIVER_MANIFEST table and, for a read-only package, a refusing
driver_command. Both follow sdm630, the one existing dual-target
precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Leitet <johan@sourceful-labs.com>
@Leitet

Leitet commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Fixed — thanks for the exact repro, it made this quick.

The scaffolder generates a dual-target package-source.json but a DRIVER-only Lua skeleton, which is how the mismatch got in. Following sdm630 (the one existing dual-target source):

  • added a DRIVER_MANIFEST table — live mirrors exactly what driver_poll emits, static matches sdm630's conservative { "make" };
  • added the refusing driver_command stub the read-only contract requires on both targets (it was reachable only after the manifest check, which is why the first error hid it).

Verified with your command: build_candidate.py --id foxess_h3_smart now answers unsigned candidate com.sourceful.driver.foxess-h3-smart@0.1.0 for blixt-l1, ftw-core. make check is green (3563 passed) and check-versions against the published beta manifest still answers added: [foxess_h3_smart], changed: [] — the driver was never published, so the byte change needs no version move.


Generated with Claude Code

@miravoss26 miravoss26 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.

New read-only Modbus driver for the Fox ESS H3-Smart / 1K5 register map, kept separate from the existing foxess driver because a 1K5 answers none of the H1/H3 map (and it replies to unknown registers with silence rather than an exception, so a wrong map fails slow instead of loud). Hardware-validated: the 1K5-HI-10-V1 capture is replayed in tests and the site-convention sign flips are asserted per stream.

Correctness. Register windows all stay in range: status 39063..39141, power 39219..39238, energy 39601..39618, pv 39279..39286, ct 38814..38815, and every i32/u32 pair lands on the documented address. Trailing all-zero PV strings are dropped, which the test pins (2 mppts, pv.W = -(3121+3475)). The single-register BMS reads for SoC and battery temp match what the block will actually answer, and give-up-after-3 is the right call for a device that pays a full timeout on a silent read.

Security. Read-only end to end: driver_command refuses every actuation, permissions is modbus.read only, control is []. No secrets, no new network destinations, no new deps. secret-scan and signed-channel checks are green.

One thing to confirm, not a blocker: the driver now calls host.decode_string and host.set_model. The changelog says both already live in the host profile and min_host_version is 1.5.0, so this is likely fine, but a missing host fn would only bite at runtime, so worth a glance that blixt-l1 exposes them at its declared min too.

Safe to merge from my read: CI green, mergeable, no unresolved threads. Not on my auto-merge allowlist, so I'll leave the merge to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fox ESS 1K5 series (1K5-HI-xx-V1) has no driver; the foxess driver's H1/H3 map gets only silence

3 participants