Design¶
Goal¶
One integration connects PV inverters, batteries and heat pumps from different vendors to Home Assistant without a cloud dependency. Vendor differences live in JSON profiles; the core only knows Modbus, decoding, entities, licensing, support and diagnostics.
Components¶
ConfigEntry
├─ Profile (JSON)
├─ Transport ── AsyncModbusTcpClient ── TCP :502
│ └─ DemoTransport (simulated)
├─ Coordinator (poll interval, UpdateFailed)
├─ LicenseManager (instance-wide)
├─ SupportAccessController (per entry)
└─ Entities
├─ RegisterSensor per visible register
├─ LicenseStatusSensor
├─ SupportRemainingSensor
├─ ConnectionBinarySensor
└─ SupportAccessSwitch
Hardware abstraction layer¶
profiles.py parses and validates profiles, groups registers into contiguous
read blocks and decodes each value with modbus_decode.py. modbus_client.py
is a small asyncio implementation of Modbus TCP:
- function 3 (read holding), 4 (read input), 6 (write single), 16 (write multiple),
- one lock per client, because gateways usually allow one socket,
- transaction id, protocol id and unit id checks,
- timeout handling that closes the socket and triggers a reconnect,
- counters for requests, errors, exceptions and latency.
scale is a divisor, matching the technical design document ("scale": 10
with raw 5000 yields 500.0).
Profiles are data¶
Built-in profiles live in custom_components/sg_energy_hub/profiles/.
Operators can drop files into <HA config>/sg_energy_hub/profiles/. Duplicate
ids and overlapping register ranges are rejected. A new vendor normally needs
no Python change.
Profiles carry their verification state. The Deye example from the design
document is shipped with "verified": false; vendor profiles are only marked
verified after a hardware test.
Licensing¶
license.py verifies Ed25519 signatures over a canonical JSON payload. The
public key is embedded in const.py; the private key stays with the vendor.
Binding: the payload contains the Home Assistant instance id
(homeassistant.helpers.instance_id). A copied config directory therefore does
not move the licence.
States:
no licence, trial < 30 days -> trial, basic features
signed licence, not expired -> active, Pro features
expired < 14 days -> grace, Pro features
expired >= 14 days -> expired, basic features
no licence, trial over -> free, basic features
gating.py filters registers (basic versus Pro) and guards write access.
Pro-only entities appear after the entry is reloaded on licence activation.
Support access¶
The controller owns the session lifetime and timeout. A provider owns the
tunnel. Version 0.1.0 ships a manual provider that never opens a network
connection; the switch and services are fully testable without exposing a
device. Tokens are requested from the licence server only when a URL is
configured, are passed to the provider and are never logged or persisted.
Diagnostics¶
diagnostics.py builds a JSON payload with:
- integration, Home Assistant, Python and platform versions,
- mode, profile metadata, poll interval, last exception and Modbus statistics,
- licence state, instance id and last check,
- active support session,
- register dump with addresses, types, units, scales and last values,
- the last 200 log lines containing
sg_energy_hub.
Keys containing password, token, secret, api_key or private are
redacted. Upload and AES-GCM packaging are not part of 0.1.0.
Error model¶
- A missing value is
None→unknown; the integration never invents0. - A failed poll raises
UpdateFailed→ all entitiesunavailable; no stale data is served. - A broken profile raises
ConfigEntryError; an unreachable device raisesConfigEntryNotReadyso Home Assistant retries. - Wire errors are
ModbusConnectionError/ModbusTimeoutError; device errors areModbusExceptionErrorwith the exception code.
Deliberate limitations¶
| Area | 0.1.0 |
|---|---|
| mDNS/Zeroconf discovery | not implemented |
| Register probing | not implemented |
| Profile sync from licence server | not implemented, last_check is recorded |
| Encrypted diagnostic upload | not implemented |
| WireGuard/Tailscale | interface only |
| Vendor profiles | demo and one unverified Deye example |
These are interfaces, not hidden features. The next steps can be added without changing the config entry or profile format.