Skip to content

Opt-in writable NIBE driver: feed PV production to the Solar PV registers (2107/2108/2109), later cap the electric add-heat #537

Description

@HuggeK

Follow-up to #530 (read-only NIBE local REST driver), #532 (heat-pump dashboard UI) and srcfl/hugin-drivers#7 (driver packaging). The shipped driver is observe-only by design — driver_command returns false and the header promises "NO control". This issue plans the opt-in write path.

Goal

Feed the pump's native Solar PV feature: periodically write live PV production into the Local REST API so the pump raises its heating / hot-water setpoints when there is surplus solar — the role NIBE's own Modbus accessory plays today, with forty-two-watts acting as that accessory.

This is control-by-hint, not direct actuation: the pump's own firmware decides what to do with the number. Misbehaviour degrades to "pump believes a wrong solar value", never to unsafe operation — which is what makes a writable v1 tractable.

Target points (seen in the S735 register map; writable flag to be confirmed in phase 0)

Point Title Role
2107 Modbus TCP/IP Ext. (Solar PV) master enable — "an external source reports PV over TCP/IP"
2108 Include own consumption (Solar PV) semantic switch: does the written value already account for house load?
2109 Available power (Solar PV) the periodically written power value
Offset heating/cooling/pool (Solar PV) user-tunable aggressiveness — stays user-owned, we don't write it
What these registers mean (semantics)

The S-series has a built-in "Solar PV" input designed for NIBE's Modbus TCP/IP accessory. When 2107 is enabled, the pump expects an external box to keep 2109 updated with the currently available solar power. 2108 tells the pump how to interpret that number:

  • 2108 off → 2109 is gross PV production; the pump subtracts its own draw when judging surplus.
  • 2108 on → 2109 is already net available power (production minus household consumption); the pump treats it as directly usable.

forty-two-watts knows both quantities from site telemetry (PV production + site meter), so either mode is implementable — the config picks one and the forwarder computes the matching value. When surplus exists, the pump applies the user-set Offset heating/cooling/pool setpoint shifts to soak it up.

Design sketch

Data flow — follows the loadpoint precedent (loadpoint.ControllerSenderFuncregistry.SendCommanddriver_command):

main dispatch cycle
  └─ solar-feed forwarder (clamp + stale guard)
       └─ SendCommand(nibe, {action:"solar_pv", power_w:…})
            └─ nibe_local.lua driver_command
                 └─ PATCH /api/v1/devices/{id}/points  {"2109": value}
  • Sign convention: PV is negative site-convention W; conversion to the pump's positive-W value happens in the driver, as always (docs/site-convention.md).
  • The forwarder is a small piece of the main loop, not a new planner: it forwards a telemetry-derived number at poll cadence. No schedule, no SoC — so it should not be a loadpoint; just a dedicated forwarder alongside the dispatch cycle.

Host API: add host.http_patch(url, body, headers) (or a generic http_request(method, …)), gated separately from read access — e.g. capabilities.http.allow_write: true. Without the grant, PATCH returns an error string exactly like ungranted MQTT/Modbus does today, so read-only remains the default even for drivers that already have http.

Opt-in — all three must hold before a single write happens:

  1. capabilities.http.allow_write: true in config.yaml (host-enforced)
  2. driver-level config.write.solar_pv: true
  3. pump-side: 2107 enabled by the owner

Safety (per docs/clamping.md, every clamp answers a quantifiable risk):

  • Clamp the written value to [0, PV nameplate] — risk: a sign bug or telemetry spike telling the pump there are 100 kW of surplus.
  • Stale guard: if site-meter or PV telemetry is stale, write 0 once, then stop — mirrors the existing stale-meter dispatch guard. Risk: pump chasing yesterday's sunshine.
  • Watchdog / DefaultMode → stop writing. Absence of writes must be the safe state; phase 0 confirms how the pump times out a silent feed.
  • Write cadence: only on change beyond a deadband, at most once per poll (60 s). Phase 0 confirms 2109 is RAM-backed, not EEPROM (flash wear).

Phases

Phase 0 — groundwork (no writes yet)

  • Enumerate all writable: true points from the live catalog (the metadata already carries the flag; one filtered bulk GET)
  • Confirm write mechanics from the pump's OpenAPI self-description at https://<ip>:8443/ — PATCH body shape, whether any pump-side mode must be enabled first, rate limits
  • Confirm what the pump does when writes stop (revert-to-no-solar timeout?) — this defines the failure mode
  • Decide default semantics: gross production vs. net surplus (2108) — leaning net surplus, since 42W computes it better than the pump can guess

Phase 1 — Solar PV feed (core of this issue)

  • host.http_patch + http.allow_write capability + a capability-denied test (pattern: TestLuaHTTPCapabilityNotGranted)
  • Solar-feed forwarder in the main loop with clamp + stale guard + deadband
  • driver_command in nibe_local.lua implementing the solar_pv action (and updating the "observe-only" header contract)
  • Config schema + docs (configuration.md, safety.md, driver docs)
  • UI: show feed status on the heat-pump card — last written value + timestamp

Phase 2 — electric add-heat (immersion heater) power limit

  • Identify the "max electrical addition" point id from the phase-0 writable catalog
  • Wire it to the fuse guard / capacity-tariff logic (the per-phase fuse clamp already exists in loadpoint) so resistive heat can be capped when the mains fuse or the power tariff is under pressure

Phase 3 — parked ideas (split into their own issues when reached)

  • Temporary lux (hot-water boost) to dump large surplus into the tank
  • Blocking add-heat during price spikes — needs care: the pump's own "smart price adaption" may already be optimising on price, and two optimisers fighting is worse than one

Test site

Writes need a site where trial-and-error is acceptable: the S735 the read-only driver was verified on is the candidate (same pump, creds and cert pin already provisioned). Everything defaults off for everyone else.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions