Skip to content

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 invents 0.
  • A failed poll raises UpdateFailed → all entities unavailable; no stale data is served.
  • A broken profile raises ConfigEntryError; an unreachable device raises ConfigEntryNotReady so Home Assistant retries.
  • Wire errors are ModbusConnectionError/ModbusTimeoutError; device errors are ModbusExceptionError with 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.