Summary
The TODO line "Multi-zone controls (currently only does single zone control, because that's what I have)" can be approached with concrete protocol-level information now. The thermostat firmware publishes the full schedule, activity, and runtime state for all 8 zones (1 through 8) on every DBIRTH/<thingName>/zones message and via NDATA/DDATA/zones updates. Zone 1 is the only one currently rendered by anantha's dashboard and HA bridge; zones 2-8 are present in the wire data and silently dropped.
This isn't a bug - the project's TODO already calls this out. This issue is a writeup of what the protocol exposes, so anyone (the maintainer or a contributor) picking up multi-zone work has a concrete starting point.
What the wire protocol carries
The DBIRTH/<id>/zones payload from a Sparkplug rebirth response contains 1050 ConfigSetting entries, structured as 8 zones x ~131 metrics each. The per-zone shape is uniform across all 8 zones:
| Group |
Count per zone |
Pattern |
| Schedule |
105 |
<N>/program/<Day>/period <P>/{time,activity,enabled} for 7 days x 5 periods x 3 fields |
| Activity setpoints |
15 |
<N>/activities/<activity>/{htsp,clsp,fan} for 5 activities x 3 fields |
| Hold/override |
3 |
<N>/hold/{hold,holdActivity,otmr} |
| Live state |
~5 |
<N>/{rt,rh,htsp,clsp,fan} (current readings) |
| Zone config |
~9 |
<N>/{enabled,name,occEnabled,occupancy,occupancyOverride,setback,tempoffset,cfmlimit,airflowlimit,damperposition,zoneconditioning,currentActivity} |
Where <N> is the zone number 1-8. Activities are away, home, manual, sleep, wake.
Plus zone-related metrics in other namespaces:
profile/zones (40 entries): zoning hardware metadata
profile/zoneboards (2 entries): zoning controller boards
sensor/zones/<N>/{favorite, tempControlSetup}: per-zone sensor config
sensor/remote/zone <N>/rt and sensor/smart/zone <N>/rt (8 each): per-zone remote/smart sensor readings
On a single-zone install (the test thermostat used for this analysis), zones 2-8 publish with enabled = false but their full schema is still present. So:
- Configured zones can be detected via
<N>/enabled = true.
- Multi-zone installs will populate zones 2-8 with real data; the protocol shape doesn't change between configurations.
Where anantha currently hardcodes zone 1
The single-zone assumption is baked into four files:
- Dashboard rendering
cmd/anantha/cmd/templates.go
RenderSchedule iterates hardcoded keys 1/program/<Day>/period <P>/{time,activity,enabled}. Other zones never make it into the rendered HTML.
RenderProfiles iterates hardcoded keys 1/activities/<activity>/{htsp,clsp,fan}. Same pattern.
- Dashboard SSE
cmd/anantha/cmd/serve.go
- The
/events handler subscribes to a hardcoded topic list including 1/clsp, 1/currentActivity, 1/fan, 1/htsp, 1/name, 1/rh, 1/rt, 1/zoneconditioning. No equivalent for zones 2-8.
- Prometheus metrics
cmd/anantha/cmd/metrics.go
zone_temp and zone_humidity gauges hardcode zone 1: zoneTempGauge.WithLabelValues("1").Set(...) driven by OnChange1("1/rt", ...).
- HA MQTT bridge
cmd/anantha/cmd/hamqtt.go
- Subscribes to
<prefix>/zone/1/{fanmode,preset_mode,temp_low,temp_high}/set for incoming HA commands.
- Publishes state on
<prefix>/zone/1/{humidity,temperature,fanmode,temp_low,temp_high,preset_mode}/current.
- The discovery JSON registers a single climate entity for zone 1.
- Writes to the thermostat use
zones/1/... topic-prefixed metric names via NCMD.
There are no zones 2-8 codepaths anywhere. The zone-1 hardcode is consistent across all of these.
What "multi-zone support" would mean concretely
The cleanest way to add multi-zone is per-zone parameterization of every existing zone-1 path:
Dashboard rendering
RenderSchedule iterates zones with <N>/enabled == true and outputs one schedule grid per enabled zone (or a tab/accordion UI if you want to keep the page compact).
RenderProfiles similarly outputs per-zone activity profile cards.
- The SSE
/events handler dynamically subscribes to the zone keys for each enabled zone rather than the hardcoded 1/... list.
Prometheus metrics
zone_temp and zone_humidity gauges already accept a zone label. The work is wiring OnChange1("<N>/rt", ...) for each enabled zone instead of just zone 1.
- Other zone-tagged metrics (humidity, setpoints, fan mode) could be added with the same per-zone-OnChange1 pattern.
HA MQTT bridge
- Discovery: publish one
homeassistant/climate/<clientID>-zone<N>/config per enabled zone, with topic strings parameterized on <N>. (Or keep the existing single-zone entity for zone 1 and add additional entities only for <N> >= 2 if backward compat matters.)
- State publishes: parameterize the existing
OnChange1("1/clsp", ...) etc. handlers across enabled zones.
- Command subscriptions: subscribe to per-zone command topics and translate to the corresponding
zones/<N>/... Carrier-side write.
The command-side write path is already partially confirmed: the HA bridge writes zones/1/hold/hold, zones/1/activities/manual/htsp, etc. via NCMD today and the firmware accepts those. By symmetry, zones/2/..., zones/3/... etc. should also be accepted, but this is a write path that nobody has exercised against the live firmware. Worth confirming on a multi-zone install before shipping.
Quick test plan for a contributor
If someone with a multi-zone install wants to validate before doing the larger UI work:
- Check
/recent on their anantha for <N>/enabled == true for some <N> other than 1. That confirms the firmware is publishing for that zone.
- Compare
<N>/program/.../, <N>/activities/... keys between zones to ensure shape parity.
- Optionally probe a write: publish a
CarrierInfo{ConfigSettings: [{Name: "zones/2/hold/hold", ConfigType: CT_BOOL, BoolValue: false}]} to spBv1.0/WallCtrl/NCMD/<clientID> and observe whether zone 2's hold state changes.
A single-zone install can also do (1) and (2) by checking that all 8 zones' schemas are present even when 7 are disabled - confirms the protocol carries everything regardless of activation.
Related: DCMD/<id>/zones is the protocol-correct alternative
Carrier's Sparkplug B mapping has a sub-device topic spBv1.0/WallCtrl/DCMD/<thingName>/zones that the thermostat subscribes to on every CONNECT. Per Sparkplug semantics this is the "preferred" route for sub-device-targeted commands - it's what would carry zone writes if a Sparkplug-spec-compliant SCADA host were the source. anantha currently uses NCMD instead and the firmware accepts that route, so multi-zone work doesn't need to adopt DCMD.
DCMD experimentation could be a separate follow-up; it's not on the critical path for multi-zone.
Reference data
The DBIRTH-level decode of zones (1050 ConfigSettings, full per-zone shape across all 8 zones) was captured locally and saved as a reference. If useful for the maintainer, the relevant protoprint output can be shared. The wire-level confirmation of zones 2-8 carrying full schema even when disabled is the key finding.
Summary
The TODO line "Multi-zone controls (currently only does single zone control, because that's what I have)" can be approached with concrete protocol-level information now. The thermostat firmware publishes the full schedule, activity, and runtime state for all 8 zones (1 through 8) on every
DBIRTH/<thingName>/zonesmessage and viaNDATA/DDATA/zonesupdates. Zone 1 is the only one currently rendered by anantha's dashboard and HA bridge; zones 2-8 are present in the wire data and silently dropped.This isn't a bug - the project's TODO already calls this out. This issue is a writeup of what the protocol exposes, so anyone (the maintainer or a contributor) picking up multi-zone work has a concrete starting point.
What the wire protocol carries
The
DBIRTH/<id>/zonespayload from a Sparkplug rebirth response contains 1050ConfigSettingentries, structured as 8 zones x ~131 metrics each. The per-zone shape is uniform across all 8 zones:<N>/program/<Day>/period <P>/{time,activity,enabled}for 7 days x 5 periods x 3 fields<N>/activities/<activity>/{htsp,clsp,fan}for 5 activities x 3 fields<N>/hold/{hold,holdActivity,otmr}<N>/{rt,rh,htsp,clsp,fan}(current readings)<N>/{enabled,name,occEnabled,occupancy,occupancyOverride,setback,tempoffset,cfmlimit,airflowlimit,damperposition,zoneconditioning,currentActivity}Where
<N>is the zone number 1-8. Activities areaway,home,manual,sleep,wake.Plus zone-related metrics in other namespaces:
profile/zones(40 entries): zoning hardware metadataprofile/zoneboards(2 entries): zoning controller boardssensor/zones/<N>/{favorite, tempControlSetup}: per-zone sensor configsensor/remote/zone <N>/rtandsensor/smart/zone <N>/rt(8 each): per-zone remote/smart sensor readingsOn a single-zone install (the test thermostat used for this analysis), zones 2-8 publish with
enabled = falsebut their full schema is still present. So:<N>/enabled = true.Where anantha currently hardcodes zone 1
The single-zone assumption is baked into four files:
cmd/anantha/cmd/templates.goRenderScheduleiterates hardcoded keys1/program/<Day>/period <P>/{time,activity,enabled}. Other zones never make it into the rendered HTML.RenderProfilesiterates hardcoded keys1/activities/<activity>/{htsp,clsp,fan}. Same pattern.cmd/anantha/cmd/serve.go/eventshandler subscribes to a hardcoded topic list including1/clsp,1/currentActivity,1/fan,1/htsp,1/name,1/rh,1/rt,1/zoneconditioning. No equivalent for zones 2-8.cmd/anantha/cmd/metrics.gozone_tempandzone_humiditygauges hardcode zone 1:zoneTempGauge.WithLabelValues("1").Set(...)driven byOnChange1("1/rt", ...).cmd/anantha/cmd/hamqtt.go<prefix>/zone/1/{fanmode,preset_mode,temp_low,temp_high}/setfor incoming HA commands.<prefix>/zone/1/{humidity,temperature,fanmode,temp_low,temp_high,preset_mode}/current.zones/1/...topic-prefixed metric names via NCMD.There are no zones 2-8 codepaths anywhere. The zone-1 hardcode is consistent across all of these.
What "multi-zone support" would mean concretely
The cleanest way to add multi-zone is per-zone parameterization of every existing zone-1 path:
Dashboard rendering
RenderScheduleiterates zones with<N>/enabled == trueand outputs one schedule grid per enabled zone (or a tab/accordion UI if you want to keep the page compact).RenderProfilessimilarly outputs per-zone activity profile cards./eventshandler dynamically subscribes to the zone keys for each enabled zone rather than the hardcoded1/...list.Prometheus metrics
zone_tempandzone_humiditygauges already accept azonelabel. The work is wiringOnChange1("<N>/rt", ...)for each enabled zone instead of just zone 1.HA MQTT bridge
homeassistant/climate/<clientID>-zone<N>/configper enabled zone, with topic strings parameterized on<N>. (Or keep the existing single-zone entity for zone 1 and add additional entities only for<N>>= 2 if backward compat matters.)OnChange1("1/clsp", ...)etc. handlers across enabled zones.zones/<N>/...Carrier-side write.The command-side write path is already partially confirmed: the HA bridge writes
zones/1/hold/hold,zones/1/activities/manual/htsp, etc. via NCMD today and the firmware accepts those. By symmetry,zones/2/...,zones/3/...etc. should also be accepted, but this is a write path that nobody has exercised against the live firmware. Worth confirming on a multi-zone install before shipping.Quick test plan for a contributor
If someone with a multi-zone install wants to validate before doing the larger UI work:
/recenton their anantha for<N>/enabled == truefor some<N>other than 1. That confirms the firmware is publishing for that zone.<N>/program/.../,<N>/activities/...keys between zones to ensure shape parity.CarrierInfo{ConfigSettings: [{Name: "zones/2/hold/hold", ConfigType: CT_BOOL, BoolValue: false}]}tospBv1.0/WallCtrl/NCMD/<clientID>and observe whether zone 2's hold state changes.A single-zone install can also do (1) and (2) by checking that all 8 zones' schemas are present even when 7 are disabled - confirms the protocol carries everything regardless of activation.
Related:
DCMD/<id>/zonesis the protocol-correct alternativeCarrier's Sparkplug B mapping has a sub-device topic
spBv1.0/WallCtrl/DCMD/<thingName>/zonesthat the thermostat subscribes to on every CONNECT. Per Sparkplug semantics this is the "preferred" route for sub-device-targeted commands - it's what would carry zone writes if a Sparkplug-spec-compliant SCADA host were the source. anantha currently usesNCMDinstead and the firmware accepts that route, so multi-zone work doesn't need to adopt DCMD.DCMD experimentation could be a separate follow-up; it's not on the critical path for multi-zone.
Reference data
The DBIRTH-level decode of
zones(1050 ConfigSettings, full per-zone shape across all 8 zones) was captured locally and saved as a reference. If useful for the maintainer, the relevantprotoprintoutput can be shared. The wire-level confirmation of zones 2-8 carrying full schema even when disabled is the key finding.