Skip to content

feat(drivers): NIBE S-series local REST API driver + opt-in HTTP TLS cert pinning#530

Merged
frahlg merged 7 commits into
srcfl:masterfrom
HuggeK:feat/nibe-local-driver
Jul 10, 2026
Merged

feat(drivers): NIBE S-series local REST API driver + opt-in HTTP TLS cert pinning#530
frahlg merged 7 commits into
srcfl:masterfrom
HuggeK:feat/nibe-local-driver

Conversation

@HuggeK

@HuggeK HuggeK commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only NIBE S-series heat-pump driver (drivers/nibe_local.lua) that reads the pump's on-prem Local REST API over the LAN — no cloud, no OAuth — plus opt-in TLS certificate pinning for the Lua HTTP host capability.

Driver

  • Auto-detects the device serial, pulls the full ~980-point register map in one request, and emits every point into the long-format TS DB with exact per-point divisor scaling (the local API ships each point's unit + divisor, so no °C×10 heuristic like the cloud driver needs).
  • Reuses the same hp_* headline metrics as the MyUplink cloud driver (hp_power_w, hp_hw_top_temp_c, hp_outdoor_temp_c, …) so the heating dashboard works with either source.
  • Read-only (no writes); self-heals device detection; per-point type guards + per-size "not connected" sentinel filtering.
  • Headline ids default to the S735 and are config-overridable per model; all other points still flow under auto-generated hp_<name> names.

Host: opt-in cert pinning

  • capabilities.http.tls_pin_sha256 pins an HTTPS endpoint's leaf certificate by SHA-256, so a driver can reach a self-signed endpoint (the pump) by trusting exactly one certificate — a swapped cert (LAN MITM) is rejected at the handshake — instead of disabling verification.
  • Strictly opt-in: drivers without a pin keep standard system-root verification (unchanged for every existing HTTP driver).

Verification

  • Live end-to-end against a real NIBE S735 (serial detected; headline metrics + lifetime COP ≈ 2.55).
  • ✅ Hermetic driver test (Basic auth, serial anchoring, exact scaling, sentinel filtering, no double-emit).
  • ✅ Unit tests: pin accept / reject / no-pin / malformed + fingerprint normalization.
  • go build ./..., go vet, gofmt clean.

Companion public-driver PR opened against srcfl/hugin-drivers.

Draft: opening for review; happy to adjust headline-id defaults / docs before merge.

🤖 Generated with Claude Code

…cert pinning

Add drivers/nibe_local.lua — read-only NIBE S-series telemetry over the
on-prem Local REST API (HTTPS + Basic auth, self-signed cert pinned). It
auto-detects the serial, pulls the full ~980-point register map in one
request, and emits every point with exact per-point divisor scaling plus
the myUplink-compatible hp_* headline metrics so the heating dashboard
works with either source. Read-only; self-heals device detection.

Add opt-in TLS certificate pinning to the Lua HTTP host capability
(capabilities.http.tls_pin_sha256): pins a self-signed endpoint's leaf
cert by SHA-256 so a driver can reach it without disabling verification.
A swapped cert (LAN MITM) is rejected at the handshake. Drivers without a
pin keep standard system-root verification — unchanged for all existing
HTTP drivers.

Verified live against a real NIBE S735 plus hermetic + unit tests
(pin accept/reject/no-pin/malformed + fingerprint normalization).

Co-Authored-By: HuggeK <[email protected]>
claude and others added 2 commits June 26, 2026 12:27
Resolve the hp_* headline variable-ids per pump from a built-in PROFILES
map keyed by product.name/firmwareId (generic S-series default, verified
on S735), refined right after device detection. Config overrides
(param_*_id) still win (override > profile > default). One driver now
covers the whole S-series with no per-model code; add a PROFILES entry
only for a model confirmed to renumber a headline.

Co-Authored-By: HuggeK <[email protected]>
…'NIBE REST API S-series'

host.emit_metric gains an optional 4th arg, register — a source address (e.g.
a Modbus register id) carried into the live metric snapshot (telemetry
MetricSnapshot.Register, json "register"). It's generic: any driver with
addressable points can pass it; existing 3-arg callers are unaffected (ocpp +
tests updated to the explicit 5-arg Store.EmitMetric form).

The NIBE driver reads metadata.modbusRegisterID per point and passes it for
every emitted signal — canonical aliases (hp_power_w -> reg 1048) and
auto-named metrics alike — so the per-driver 'all signals' detail view can show
a Register column. Menu-only points (register 0 / absent) emit "" so the
column stays blank rather than showing a misleading 0.

Also renames the driver's UI display name to 'NIBE REST API S-series'.

Co-Authored-By: HuggeK <[email protected]>
@HuggeK
HuggeK force-pushed the feat/nibe-local-driver branch from eb63a1a to a5869de Compare June 26, 2026 13:16
parseRange capped at 3d, so the heat-pump month temp graph (30d) and the
energy-period windows (7d/30d/366d) silently fell back to 5m. Add a generic
'<N><unit>' parser (m/h/d/w/y) after the existing fixed cases, capped at 2y to
bound the TS-DB scan. Existing exact ranges are unchanged.

Co-Authored-By: HuggeK <[email protected]>
@HuggeK

HuggeK commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Future work: optional writable mode — Solar PV / smart-energy control

The driver is read-only today (by design). When/if we add an opt-in writable path, the NIBE local API exposes a useful set of Solar PV control holding registers that would let 42w push live PV surplus into the pump (so it stores free solar as heat / hot water / pool instead of exporting):

Register modbus Purpose
Modbus TCP/IP Ext. (Solar PV) 2107 Enable — tells the pump an external Modbus source controls Solar PV. Must be on for the rest to take effect.
…Available power (Solar PV) 2109 The available PV power (kW) we feed in — what the pump may divert.
…Include own consumption (Solar PV) 2108 Flag controlling how Available power is interpreted (see below).
Offset heating / cooling / pool (Solar PV) 2668 / 2667 / 2669 How far the pump shifts each target to absorb the surplus.
PV panel affects heating / hot water (EME) 1069 / 1070 EME on/off toggles.
Include own consumption (Solar PV) (2108) — important semantics
  • on → we send gross PV production, and the pump subtracts the household's own consumption itself to derive the surplus.
  • off → we send the net surplus (already after household consumption) directly.

The flag must match how we computed the number we write, otherwise the wrong amount of surplus gets dumped into heating / hot water / pool.

Caveats for the writable path

  • It's a control write — needs the explicit opt-in + safety story we deliberately avoided for the read-only driver.
  • The external EME feed times out (~6 min)Available power must be refreshed periodically or the pump reverts to its own sensors.
  • Need a test rig — validating writes safely needs a NIBE test site / spare unit; writing setpoints to a live, in-use pump isn't something to iterate on. (Help wanted.)

claude and others added 2 commits June 29, 2026 14:58
The network scan only dialed Modbus 502 / MQTT 1883 / HTTP 80, so a NIBE
S-series pump — reachable, but serving its Local REST API on 8443 — never
appeared in Settings -> Scan or the setup wizard. Add 8443 (https) to the
probe set.

Co-Authored-By: HuggeK <[email protected]>
host.emit_metric gains an optional 5th title arg (the device's own point
label). It threads through metricSnap -> MetricSnapshot -> /api/drivers/{name}
so the heat-pump all-signals view can show a plain-language clarification under
each of the ~960 raw signals. The NIBE driver passes pt.title; all other
EmitMetric callers pass "" (ocpp, tests).

Co-Authored-By: HuggeK <[email protected]>
@HuggeK

HuggeK commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

🔗 Paired with #532 (dashboard UI)

This driver is the data source; the dashboard UI that renders its telemetry is #532. Best reviewed / merged together:

Without #530 the card has no data; without #532 the telemetry has no UI.

@HuggeK

HuggeK commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Now out of draft (together with the UI PR #532 and the Hugin packaging PR srcfl/hugin-drivers#7). The opt-in writable follow-up — feeding live PV production into the pump's Solar PV registers (2107/2108/2109), later a power cap for the electric add-heat — is planned in #537.

@frahlg frahlg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed driver boundary, TLS pinning, telemetry/storage behavior, configuration, and tests. Added maintainer fixes so configured pins fail closed and cannot downgrade to HTTP, the ~980-point map records changes plus bounded full snapshots instead of duplicate rows every minute, sanitized-name collisions are stable, and range parsing cannot overflow. Local verify-all and GitHub CI are green.

@frahlg
frahlg merged commit 33d0608 into srcfl:master Jul 10, 2026
2 checks passed
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.

3 participants