Skip to content

feat(wiser_heat): read-only Wiser heating hub over its local HTTPS API - #222

Open
hannesb90 wants to merge 6 commits into
srcfl:mainfrom
hannesb90:feat/wiser-heat
Open

hannesb90 wants to merge 6 commits into
srcfl:mainfrom
hannesb90:feat/wiser-heat

Conversation

@hannesb90

@hannesb90 hannesb90 commented Nov 10, 2026 •

Copy link
Copy Markdown

Summary

A new read-only driver for the Schneider Electric / Drayton Wiser heating
hub over its local HTTPS API. Toward srcfl/ftw#520: room temperatures from a
thermostat system FTW cannot read today, for a later heat planner to judge
comfort by.

One GET of https://<hub>/data/domain/ per minute, the hub's whole state as
JSON. The minute is set with host.set_poll_interval (30 s after a
failure): an interval returned from driver_poll is capped at 30 s by the
host, which the first commit relied on by mistake. It emits metrics only, no power stream:

  • room_<id>_temp_c and room_<id>_setpoint_c for each room, plus
    room_<id>_demand_pct, room_<id>_output_on and room_<id>_temp_is_floor
    (1 when the room is controlled on floor temperature, 0 on air)
  • heating_actuator_<id>_floor_temp_c, _floor_max_c and _floor_min_c for
    each floor thermostat with a floor sensor
  • heating_actuator_<id>_power_est_w when the owner states the load
    (floor_heating: [{actuator, rated_w | area_m2 [, w_per_m2 = 110]}]): the
    load times the room's demand, the average draw over the relay's ten-minute
    cycle. The CFMT measures no power (the hub reports InstantaneousDemand: 0
    with the relay on), so this is an estimate and stays a metric, never a power
    stream. On the hub with a stated 4.5 m² it emitted 0 W at 0 % demand; a
    non-zero value is covered in the harness only.
  • smoke_alarm_<id>_temp_c for each smoke alarm
  • heating_channel_<id>_demand_pct and heating_channel_<id>_relay_on for
    each heating channel
  • wiser_poll_ok

Ids are the hub's own. Room names are owner text and stay out of metric names.

Driver evidence

  • Driver id: wiser_heat 0.1.0, community tier.
  • Device make/model: Wiser Hub R gen 2, WT704R1B30S4, with smoke alarms
    (firmware 2.7.6) and, since 2026-11-11, one Renova Connected Thermostat
    16 A (WDE011680, firmware 1.0.59) on electric floor heating with a 15 kOhm
    floor sensor. No radiator valve or room stat is paired.
  • Firmware tested: hub 4.50.4.
  • Protocol/register source: the hub's own /data/domain/ answer, read on
    hardware. Schneider publishes no document for this local API, so the
    manifest has no upstream_docs. The sensor-error value 2000 comes from the
    Wiser community libraries, not from the test hub.
  • Hardware test performed, 2026-11-10:
    • The driver ran through FTW's POST /api/drivers/test against the hub,
      with the hub's certificate pinned via tls_pin_sha256 and the secret in
      config: smoke alarm 21.0 °C, heating channel demand 0 % and relay off,
      and the empty room correctly not emitted. With a wrong secret the hub
      answers 401, and the driver emits only wiser_poll_ok = 0.
    • 2026-11-11, with the floor thermostat: the driver ran through
      POST /api/drivers/test on FTW v0.157.3-beta.1 against the same hub, in
      preset P6 (room control with floor limit): room 24.2 °C, setpoint
      20.0 °C, temp_is_floor 0, demand 0 % and output off, floor sensor
      26.2 °C, floor upper limit 30.0 °C. The room air reading matched the
      thermostat's display, and the floor reading matched an IR reading of the
      floor surface (26.6-27.5 °C). Before, in preset P8 (floor control), the
      hub showed the room temperature as the floor sensor's
      (InputTemperatureType: Floor) with demand 100 % and output on, logged
      over 20 minutes; that state is covered in the harness.
    • The floor sensor type matters and the hub cannot see it: the 15 kOhm
      probe configured as NTC_10K read 17.4 °C against 26.6-27.5 °C on the
      floor, and 27.0 °C once set to NTC_15K. The driver logs the type when it
      changes.
    • make test-driver ID=wiser_heat and make check pass.
      invented-number-report: nothing invented.
    • A Lua harness test uses that hub's answer, with identifiers replaced, and
      varies single fields for what the hub could not show: a room with a
      thermostat, a relay that is on, a sensor error, an offline device, no
      answer, a non-JSON answer, and no secret configured. A deliberate break of
      the "no temperature" check fails it.
  • Known limits:
    • Radiator valves (iTRV) and room stats are decoded only as rooms, not
      seen on hardware.
      The floor lower limit is decoded but was not
      configured on the test thermostat. A room with nothing
      assigned reports CalculatedTemperature: -32768 and
      CurrentSetPoint: -200, and neither is emitted. Wiser also uses -200
      (-20.0 °C) for a room set to off, so a setpoint at or below it is
      treated as no setpoint.
    • Staleness relies on the hub's own signal field. The hub answers from
      its cache, so a device whose DisplayedSignalStrength is Offline or
      NoSignal has its temperature left out. How quickly the hub marks a
      silent device offline is not measured.
    • Getting the secret is a manual step. The hub hands it out on
      GET http://<hub>/secret only in setup mode (the Setup button; it ends
      itself after 15 minutes). The driver comment states the steps. A setup
      flow in FTW's UI would be a separate Core change.
    • ders: [heatpump] is the closest of the allowed DER kinds; the hub is a
      heating controller, not a heat pump.

Safety

  • The driver starts read-only, or this is a separately reviewed control change.
  • Vendor signs are converted at the driver boundary. (No power stream is emitted.)
  • Cached telemetry becomes stale instead of being re-emitted as fresh.
  • No credentials, serial numbers, private addresses or site data are included.

The secret is declared in config_secrets, so FTW masks it. Only GET is used.

Control evidence

  • HIL evidence or required: not applicable, read-only.
  • Control changes include default-mode, lease-expiry and HIL evidence.

Checks

  • Commits include Signed-off-by.
  • make test-driver ID=wiser_heat
  • make check

🤖 Generated with Claude Code

hannesb90 and others added 2 commits November 10, 2026 11:52
One GET of /data/domain/ per minute: each room's temperature and
setpoint, each smoke alarm's temperature, and each heating channel's
demand and relay state. The hub's secret is a masked config value and
its self-signed certificate is pinned with tls_pin_sha256.

A room with nothing assigned reports -32768 and a -20.0 °C setpoint;
neither is emitted. An offline device's cached temperature is left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: hannesb90 <bruhn90@gmail.com>
The host caps an interval returned from driver_poll at 30 s, so returning
60000 alone polled the hub every 30 s, twice as often as meant. Each poll
now sets its next interval with host.set_poll_interval (60 s, or 30 s
after a failure) and returns the same value. The test checks the
interval the host is told, not only the one returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: hannesb90 <bruhn90@gmail.com>
@hannesb90
hannesb90 marked this pull request as ready for review November 10, 2026 16:15

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

Adds a read-only Schneider/Drayton Wiser hub driver over the hub's local HTTPS API: room temps and setpoints, smoke alarm temps, heating channel demand.

  • Security: the hub secret is read from config and masked via config_secrets, and sent only as a SECRET header to the configured hub. The self-signed cert is handled with tls_pin_sha256 and allowed_hosts, which is the right way to do it. No writes, no new dependencies.
  • Correctness: a failed or non-JSON answer emits only wiser_poll_ok=0, so stale values are not republished. Good.
  • Question: the base_url override is described as a test hook. If it can be set in a production config it bypasses the host-built URL, though allowed_hosts should still fence it. Worth confirming that is enforced.
  • Note: room temperatures from real thermostats are decoded but not yet seen on hardware (stated in verification_notes). The experimental status fits.

Safe to merge from my read. No CI checks were attached when I looked.

hannesb90 and others added 2 commits November 11, 2026 18:26
A Renova CFMT 16 A floor thermostat is now paired with the test hub, so
room temperatures from a real thermostat are seen on hardware.

Each room also reports its demand, its output state and whether its
temperature is floor or air (room_<id>_temp_is_floor): in a floor-control
preset the room temperature is the floor sensor's. Each heating actuator
with a floor sensor reports the floor temperature, while the sensor is in
normal operation, and its upper and lower limits. An offline thermostat's
cached values are left out.

The hub reports the configured floor sensor type but cannot see the probe.
On the test site a 15 kOhm probe configured as 10 kOhm read 17.4 °C against
26.6-27.5 °C measured on the floor; the type is logged when it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: hannesb90 <bruhn90@gmail.com>
The base_url test hook could also be set in a site's config. The request
carries the hub's secret, and FTW lets a driver with an empty allowed_hosts
reach any address, so a base_url could send the secret elsewhere or over
clear-text HTTP. The URL is now always https://<host>[:port]/data/domain/,
and the tests configure host instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: hannesb90 <bruhn90@gmail.com>
@hannesb90

Copy link
Copy Markdown
Author

Thanks. It was not fenced: FTW's http_get allows any host when allowed_hosts is empty (go/internal/drivers/lua.go:1174), so a base_url in a site config could send the hub secret elsewhere, or over plain HTTP without a pin. 17989df removes the override. The URL is now always https://<host>[:port]/data/domain/, and a harness case checks that a configured base_url is ignored.

hannesb90 and others added 2 commits November 11, 2026 19:28
A Renova CFMT switches its load with a relay and measures nothing: the hub
reports InstantaneousDemand 0 with the relay on. The owner can now state
the load per actuator, as rated_w or as area_m2 x w_per_m2 (default
110 W/m²), and the driver emits heating_actuator_<id>_power_est_w, the load
times the room's PercentageDemand. The thermostat runs the relay
time-proportionally over a ten-minute cycle, so that is the average draw
over the cycle.

It is a metric, not a power stream, so an estimate never enters a measured
site balance. A load outside 16 A x 230 V, an area over 200 m² or over
300 W/m² is refused and logged. Nothing is emitted for an actuator with no
stated load, an offline thermostat, or a poll the hub did not answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: hannesb90 <bruhn90@gmail.com>
…he output

A Renova CFMT held off by its floor limit reports Output_Thermal_Limit, and
its probe still reads (23.8 °C on the entrance floor on 2026-11-11). The
driver dropped the floor temperature exactly then. It is now kept, and
heating_actuator_<id>_floor_limit_active says the limit is holding.

Also documented from the same site: the air setpoint range follows the
floor limits, notes for a later timed-override control, and three
thermostats with 10, 12 and 15 kOhm probes in the verification notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hannesb90

Copy link
Copy Markdown
Author

Two more site findings in 93d6cac. A bug fix: a Renova CFMT held off by its floor limit reports Output_Thermal_Limit, and its probe still reads, but the driver dropped the floor temperature exactly then. It is now kept, with heating_actuator_<id>_floor_limit_active (new harness case, which fails on the old code). Docs only: the air setpoint range follows the floor limits, notes for a later timed-override control, and three thermostats (10, 12 and 15 kOhm probes, all within 0.7 °C of the floor surface) in the verification notes. index.yaml and devices.yaml are regenerated.

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

Read-only Wiser heating hub driver: one GET of /data/domain/ per poll, metrics only, no control path.

  • Security screen: clean from my read. Hub secret is a masked config value, the self-signed cert is pinned via tls_pin_sha256, and no new hosts beyond the configured hub. The GET http://<hub>/secret setup-mode step is documented operator-side only, not driver code. Good.
  • Sentinels (-32768 / -200 / 2000) are dropped rather than emitted, and offline devices don't serve stale temps. Nice, that's the failure mode that usually bites.
  • Nit: power_est_w is documented as an estimate that must not enter a measured site balance. Worth making sure nothing downstream sums *_power_est_w with real power metrics.

Safe to merge from my read (I did not run the Lua harness; trust CI for that).

This branch has not been deployed

No deployments
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.

2 participants