The GraphQL API is implemented and served by cmd/gateway. The gateway exposes:
/graphqlfor queries + mutations/graphql/subscriptionsfor subscriptions over WebSocket or SSE
This page owns the GraphQL-specific observe-first parity contract that is
currently merged on main.
- The merged M3 bus-observability fields
busSummary,busMessages, andbusPeriodicityare frozen below against gatewaymainat mainline commit83e9c7b1ba927a282d87599269e91be817ff3582(Project-Helianthus/helianthus-ebusgateway#379). - The merged M5 watch-summary root field
watchSummaryis frozen below against gatewaymainat mainline commit92b3576c9203bf5a02a45494e935041961044600(Project-Helianthus/helianthus-ebusgateway#393). ISSUE-GW-05/ Project-Helianthus/helianthus-ebusgateway#378 is the runtime-owning lane for the M3 bus-observability GraphQL parity surface (busSummary,busMessages,busPeriodicity).ISSUE-GW-11is the runtime-owning lane for the M5 watch-summary GraphQL parity surface (watchSummary).watch-summary.mdowns the shared watch-summary contract.ISSUE-DOC-07freezes the current bus-observability GraphQL contract, andISSUE-DOC-09freezes watch-summary GraphQL behavior.
The builder assembles a GraphQL-oriented schema model directly from the registry:
- Registry-driven types: devices, planes, and methods are enumerated from the registry; each method includes frame primary/secondary bytes and a response schema (fields + data types) selected via the schema selector for the device’s address/hardware version.
- Projections in snapshots: projection graphs are included in each schema snapshot (
Device.projections) alongside planes and methods. - Projection validation: projections are validated during schema build; invalid projection graphs fail the build and surface a schema error.
- Rebuild on registry change:
Start(ctx)performs an initial build and listens on achangeschannel; each signal triggers a rebuild. If no channel is provided, callers can trigger rebuilds viaRebuild(). - Graceful channel close: if the
changeschannel is closed, the builder stops listening instead of spinning rebuilds. - Revisioned snapshots: each successful rebuild increments a revision counter;
Schema()returns a deep-copied snapshot to keep callers insulated from concurrent rebuilds.
This excerpt freezes the ISSUE-DOC-07 + ISSUE-DOC-09 observe-first subset
of the current merged Query surface. It is not a complete Query
definition.
type Query {
busSummary: BusSummary
busMessages(limit: Int): BusMessagesList
busPeriodicity(limit: Int): BusPeriodicityList
watchSummary: WatchSummary!
}This DOC-07 section freezes only the narrow M3 GraphQL slice shipped by the
merged gateway runtime:
busSummarybusMessages(limit: Int)busPeriodicity(limit: Int)
Behavioral invariants:
- The two list roots keep bounded-list parity with MCP.
countandcapacitydescribe the whole retained store, not just the returned slice. - When
limitis present it must be a positive integer; the gateway returns the newest retainedlimititems in retained order. Omittinglimitreturns all currently retained items and nothing more. - Current passive capability, warmup, degraded, and timing-quality state stay
explicit through the top-level
statusobject on all three roots. Retained message/periodicity history does not imply current passive health. - When no real bus-observability provider is wired, the current runtime still
returns zero-value wrapper objects for all three roots;
statusisnull,count/capacitystay0,itemsis[], andbusSummary.countersremains"0"/"0". - Within one GraphQL operation, all three roots resolve from one shared
bus-observability snapshot, so
busSummary,busMessages, andbusPeriodicitystay internally consistent for that request. - GraphQL preserves the same timing-quality semantics as the real
bus-observability store and MCP adapter. The merged runtime proves
estimatedandunavailable; these fields must not imply exact wire-time precision.
Current value/encoding rules:
status.capability.passiveStateandstatus.warmup.stateuse the bounded state setunavailable | warming_up | available.- Current passive-unavailability reasons match the MCP freeze:
startup_timeout,reconnect_timeout,socket_loss,flap_dampened,unsupported_or_misconfigured, andcapability_withdrawn. status.degraded.reasonsmay include those passive-unavailability reasons anddedup_degraded.seriesBudgetOverflowTotalandperiodicityBudgetOverflowTotalare decimal strings, not GraphQL integers.observedAtandlastSeenare UTC RFC3339 strings; the runtime uses RFC3339Nano formatting, so fractional seconds appear only when present.lastInterval,meanInterval,minInterval, andmaxIntervalare duration strings (for example5s) and remain omitted until the runtime has a value.
This DOC-09 section freezes the merged M5 GraphQL watch-summary root:
watchSummary
Behavioral invariants:
watchSummaryis always present as a non-null GraphQL root field.- If the runtime watch provider is unwired,
watchSummaryresolves to zero values and empty lists (notnull). - Within one GraphQL operation, multiple
watchSummaryselections resolve from one shared snapshot; duplicated aliases do not observe intra-operation skew. - Portal-specific query cadence/bootstrap behavior is out of scope in this
file and remains owned by
DOC-10.
Current value/encoding rules:
- GraphQL names are camelCase (
activationCounts,freshnessClasses,directApplyEligibilityClasses,shadowingEnabled). freshnessClasses,directApplyEligibilityClasses,inventory.stateClasses,inventory.pinClasses,activationCounts.sourceClasses, anddegraded.reasonsare non-null lists.- Class labels and semantics are frozen in
watch-summary.md.
type WatchSummary {
inventory: WatchSummaryInventory!
activationCounts: WatchSummaryActivationCounts!
freshnessClasses: [WatchSummaryClassCount!]!
directApplyEligibilityClasses: [WatchSummaryClassCount!]!
degraded: WatchSummaryDegraded!
}
type WatchSummaryClassCount {
class: String!
count: Int!
}
type WatchSummaryInventory {
totalEntries: Int!
pinnedEntries: Int!
evictableEntries: Int!
staticPinnedFootprint: Int!
writeConfirmPinnedActive: Int!
stateClasses: [WatchSummaryClassCount!]!
pinClasses: [WatchSummaryClassCount!]!
}
type WatchSummaryActivationCounts {
catalogDescriptors: Int!
activeKeys: Int!
sourceClasses: [WatchSummaryClassCount!]!
}
type WatchSummaryDegraded {
active: Boolean!
shadowingEnabled: Boolean!
pinnedBudgetDegraded: Boolean!
compactorDegraded: Boolean!
reasons: [String!]!
}type BusSummary {
status: BusObservabilityStatus
messages: BusBoundedListSummary!
periodicity: BusBoundedListSummary!
counters: BusObservabilityCounters!
}
type BusObservabilityStatus {
transportClass: String!
capability: BusObservabilityCapability!
warmup: BusObservabilityWarmup!
timingQuality: BusObservabilityTimingQuality!
degraded: BusObservabilityDegraded!
}
type BusObservabilityCapability {
activeSupported: Boolean!
passiveSupported: Boolean!
broadcastSupported: Boolean!
passiveAvailable: Boolean!
passiveState: String!
passiveReason: String
endpointState: String!
tapConnected: Boolean!
}
type BusObservabilityWarmup {
state: String!
blocker: String
elapsedSeconds: Float
completedTransactions: Int!
requiredTransactions: Int!
completionMode: String
}
type BusObservabilityTimingQuality {
active: String!
passive: String!
busy: String!
periodicity: String!
}
type BusObservabilityDegraded {
active: Boolean!
reasons: [String!]!
}
type BusBoundedListSummary {
count: Int!
capacity: Int!
}
type BusObservabilityCounters {
seriesBudgetOverflowTotal: String!
periodicityBudgetOverflowTotal: String!
}
type BusMessagesList {
status: BusObservabilityStatus
count: Int!
capacity: Int!
items: [BusMessage!]!
}
type BusMessage {
scope: String!
family: String!
frameType: String!
outcome: String!
observedAt: String
sourceAddress: Int!
targetAddress: Int!
requestLen: Int!
responseLen: Int!
}
type BusPeriodicityList {
status: BusObservabilityStatus
count: Int!
capacity: Int!
items: [BusPeriodicityEntry!]!
}
type BusPeriodicityEntry {
sourceBucket: String!
targetBucket: String!
primary: Int!
secondary: Int!
family: String!
state: String!
lastSeen: String
sampleCount: Int!
lastInterval: String
meanInterval: String
minInterval: String
maxInterval: String
}type Device {
address: Int!
addresses: [Int!]!
manufacturer: String!
deviceId: String!
serialNumber: String
macAddress: String
softwareVersion: String!
hardwareVersion: String!
displayName: String
productFamily: String
productModel: String
partNumber: String
role: String
planes: [Plane!]!
projections: [Projection!]!
}
type Plane {
name: String!
methods: [Method!]!
}
type Method {
name: String!
readOnly: Boolean!
primary: Int!
secondary: Int!
response: ResponseSchema!
}
type ResponseSchema {
fields: [Field!]!
}
type Field {
name: String!
type: String!
size: Int!
}
type ServiceStatus {
status: String!
firmwareVersion: String
updatesAvailable: Boolean!
initiatorAddress: String
}
type Projection {
plane: String!
nodes: [ProjectionNode!]!
edges: [ProjectionEdge!]!
}
type ProjectionNode {
id: String!
path: String!
canonicalPath: String!
}
type ProjectionEdge {
id: String!
from: String!
to: String!
}
type Zone {
id: String!
name: String!
instance: Int!
state: ZoneState
config: ZoneConfig
}
type ZoneState {
currentTempC: Float
currentHumidityPct: Float
}
type ZoneConfig {
operatingMode: String
targetTempC: Float
heatingMode: String
quickVeto: Boolean
quickVetoSetpoint: Float
quickVetoExpiry: String
}
type Dhw {
operatingMode: String
currentTempC: Float
targetTempC: Float
state: String
config: String
}
type EnergyTotals {
gas: EnergyCategory
electric: EnergyCategory
solar: EnergyCategory
}
type EnergyCategory {
dhw: EnergyBucket
climate: EnergyBucket
}
type EnergyBucket {
today: Float
yearly: [Float!]!
monthly: [Float!]!
}
type BoilerStatus {
state: BoilerState
config: BoilerConfig
diagnostics: BoilerDiagnostics
}
type BoilerState {
flowTemperatureC: Float
returnTemperatureC: Float
centralHeatingPumpActive: Boolean
waterPressureBar: Float
externalPumpActive: Boolean
circulationPumpActive: Boolean
gasValveActive: Boolean
flameActive: Boolean
diverterValvePositionPct: Float
fanSpeedRpm: Int
targetFanSpeedRpm: Int
ionisationVoltageUa: Float
dhwWaterFlowLpm: Float
dhwDemandActive: Boolean
heatingSwitchActive: Boolean
storageLoadPumpPct: Float
modulationPct: Float
primaryCircuitFlowLpm: Float
flowTempDesiredC: Float
dhwTempDesiredC: Float
stateNumber: Int
dhwTemperatureC: Float
dhwTargetTemperatureC: Float
}
type BoilerConfig {
dhwOperatingMode: String
flowsetHcMaxC: Float
flowsetHwcMaxC: Float
partloadHcKW: Float
partloadHwcKW: Float
}
type BoilerDiagnostics {
heatingStatusRaw: Int
dhwStatusRaw: Int
centralHeatingHours: Float
dhwHours: Float
centralHeatingStarts: Int
dhwStarts: Int
pumpHours: Float
fanHours: Float
deactivationsIFC: Int
deactivationsTemplimiter: Int
}
Boiler field provenance is documented in [`protocols/vaillant/ebus-vaillant-B509-boiler-register-map.md`](../protocols/vaillant/ebus-vaillant-B509-boiler-register-map.md). The current contract is hybrid: direct BAI00 B509 is authoritative for most boiler fields, while a small set of controller-mirrored B524 values still feed `dhwTemperatureC`, `dhwTargetTemperatureC`, `dhwOperatingMode`, and `heatingStatusRaw`.
type SystemStatus {
state: SystemState
config: SystemConfig
properties: SystemProperties
}
type SystemState {
systemOff: Boolean
systemWaterPressure: Float
systemFlowTemperature: Float
outdoorTemperature: Float
outdoorTemperatureAvg24h: Float
maintenanceDue: Boolean
hwcCylinderTemperatureTop: Float
hwcCylinderTemperatureBottom: Float
}
type SystemConfig {
adaptiveHeatingCurve: Boolean
alternativePoint: Float
heatingCircuitBivalencePoint: Float
dhwBivalencePoint: Float
hcEmergencyTemperature: Float
hwcMaxFlowTempDesired: Float
maxRoomHumidity: Int
}
type SystemProperties {
systemScheme: Int
moduleConfigurationVR71: Int
}
type CircuitStatus {
index: Int!
circuitType: String!
hasMixer: Boolean!
state: CircuitState!
config: CircuitConfig!
managingDevice: CircuitManagingDevice!
}
type CircuitManagingDevice {
role: ManagingDeviceRole!
deviceId: String
address: Int
}
enum ManagingDeviceRole {
REGULATOR
FUNCTION_MODULE
UNKNOWN
}
type CircuitState {
pumpActive: Boolean
mixerPositionPct: Float
flowTemperatureC: Float
flowSetpointC: Float
calcFlowTempC: Float
circuitState: String
humidity: Float
dewPoint: Float
pumpHours: Float
pumpStarts: Int
}
type CircuitConfig {
heatingCurve: Float
flowTempMaxC: Float
flowTempMinC: Float
summerLimitC: Float
frostProtC: Float
coolingEnabled: Boolean
roomTempControl: String
}vr71CircuitStartIndex is intentionally absent from the canonical GraphQL contract. Circuit ownership is modeled explicitly on each circuits[] item via managingDevice, not through a global FM5 threshold.
The architectural rationale and the full B524 evidence trail for structure/ownership decisions are documented in:
energyTotals is available directly on Query and returns the same canonical energy aggregate exposed to MCP.
Example:
query {
energyTotals {
gas {
dhw { today yearly monthly }
climate { today yearly monthly }
}
electric {
dhw { today yearly monthly }
climate { today yearly monthly }
}
solar {
dhw { today yearly monthly }
climate { today yearly monthly }
}
}
}Address semantics:
addressis the canonical primary eBUS address for the physical device node.addressescontains canonical + alias faces observed for that same device.device(address:),planes(address:), andmethods(address:, plane:)accept either the canonical address or any alias address fromaddresses.
daemonStatus.initiatorAddressreports the configured eBUS initiator source used by gateway reads/scans.- Value format is
0xNNwhen resolved from runtime configuration. - In auto-leased proxy mode where the initiator is negotiated downstream, the field may return
auto.
The semantic runtime distinguishes cache bootstrap from live updates during startup.
busSummary.status.startup { phase cacheEpoch liveEpoch }exposes the machine-readable startup readiness surface used by proof-mode orchestration and Portal parity.- Cache-backed semantic payload may be published first and treated as stale bootstrap data.
- Runtime transitions through startup phases (
BOOT_INIT→CACHE_LOADED_STALE→LIVE_WARMUP→LIVE_READY, withDEGRADEDtimeout fallback). - If
-boot-live-timeoutelapses beforeLIVE_READY, runtime entersDEGRADEDuntil live epochs recover. - Successful
ebusd-tcpfallback hydration fromgrab result all(zones/DHW) is classified as live runtime data. - Energy broadcast updates do not advance startup live epochs and cannot promote phase readiness by themselves.
LIVE_READYrequires live-backed updates for each published semantic stream (zones and/or DHW), not justlive_epoch >= 2.- Persistent semantic preload is read from
-semantic-cache-pathand loaded as stale (CACHE_LOADED_STALE) when valid. - Zone visibility is hysteresis-based:
N_missconsecutive misses before removal andN_hitconsecutive hits before re-introduction (-semantic-zone-presence-miss-threshold,-semantic-zone-presence-hit-threshold). - Transient single-miss/single-hit alternation keeps zones stable and avoids entity flapping.
- Zone/DHW semantic publication uses non-destructive incremental merge: failed attempted fields retain last-known values instead of being wiped by partial snapshots.
- Freshness is tracked per merged field in runtime state; GraphQL currently exposes merged values and startup phase/state contracts.
- DHW retains last-known values during cache-only/transient gaps until
-semantic-dhw-stale-ttlis exceeded, thendhwis explicitly cleared.
Example startup-readiness query:
query {
busSummary {
status {
startup {
phase
cacheEpoch
liveEpoch
}
}
}
}Authoritative startup FSM and transition details are documented in architecture/startup-semantic-fsm.md.
Zone lifecycle details are documented in architecture/zone-presence-fsm.md.
DHW lifecycle details are documented in architecture/dhw-freshness-fsm.md.
Structural family/instance discovery rules are documented in ../architecture/semantic-structure-discovery.md and ../architecture/b524-structural-decisions.md.
- ProjectionNode.id derives from the canonical Service path for the node (stable across projections).
- ProjectionEdge.from/to refer to node IDs within the same projection.
- path / canonicalPath format:
Plane:/segment@value/segment@value/...wherePlaneis the projection plane name and each segment may include an@-qualified locator.canonicalPathalways usesServiceas the plane.
Projections are plane-scoped graphs attached to each device. The API exposes:
- planes:
Projection.planeidentifies the view (e.g.,Service,Observability,Debug). - nodes:
Projection.nodesis the list of graph nodes;pathis plane-local for display, whilecanonicalPathanchors identity in theServiceplane. - edges:
Projection.edgesconnects nodes within the same plane;fromandtoare node IDs. - canonical paths:
ProjectionNode.idis derived from thecanonicalPath, so the same node ID appears across planes when they represent the same canonical entity in a snapshot.
query ProjectionBrowserProjections($address: Int!) {
device(address: $address) {
address
addresses
manufacturer
deviceId
projections {
plane
nodes {
id
path
canonicalPath
}
edges {
id
from
to
}
}
}
}{
"data": {
"device": {
"address": 50,
"addresses": [50, 236],
"manufacturer": "Vaillant",
"deviceId": "BASV2",
"projections": [
{
"plane": "Service",
"nodes": [
{
"id": "Service:/ebus/addr@50/device@BASV2",
"path": "Service:/ebus/addr@50/device@BASV2",
"canonicalPath": "Service:/ebus/addr@50/device@BASV2"
},
{
"id": "Service:/ebus/addr@50/device@BASV2/method@get_operational_data",
"path": "Service:/ebus/addr@50/device@BASV2/method@get_operational_data",
"canonicalPath": "Service:/ebus/addr@50/device@BASV2/method@get_operational_data"
}
],
"edges": [
{
"id": "Service:Service:/ebus/addr@50/device@BASV2->Service:/ebus/addr@50/device@BASV2/method@get_operational_data",
"from": "Service:/ebus/addr@50/device@BASV2",
"to": "Service:/ebus/addr@50/device@BASV2/method@get_operational_data"
}
]
},
{
"plane": "Observability",
"nodes": [
{
"id": "Service:/ebus/addr@50/device@BASV2",
"path": "Observability:/ebus/addr@50/device@BASV2",
"canonicalPath": "Service:/ebus/addr@50/device@BASV2"
},
{
"id": "Service:/ebus/addr@50/device@BASV2/method@get_operational_data",
"path": "Observability:/ebus/addr@50/device@BASV2/method@get_operational_data",
"canonicalPath": "Service:/ebus/addr@50/device@BASV2/method@get_operational_data"
}
],
"edges": [
{
"id": "Observability:Service:/ebus/addr@50/device@BASV2->Service:/ebus/addr@50/device@BASV2/method@get_operational_data",
"from": "Service:/ebus/addr@50/device@BASV2",
"to": "Service:/ebus/addr@50/device@BASV2/method@get_operational_data"
}
]
}
]
}
}
}The gateway exposes a lightweight HTTP endpoint to fetch a single projection graph from the latest schema snapshot (outside GraphQL). The default path is /snapshot and can be configured via -snapshot-path.
Request
- Method:
GET - Query params:
address(required): device address as decimal or hex (e.g.,16or0x10)plane(required): projection plane name (e.g.,Service,Observability,Debug)
GET /snapshot?address=0x10&plane=Observability
Accept: application/json
{
"address": 16,
"plane": "Observability",
"nodes": [
{
"id": "Service:/ebus/addr@16/device@BASV2",
"path": "Observability:/ebus/addr@16/device@BASV2",
"canonicalPath": "Service:/ebus/addr@16/device@BASV2"
},
{
"id": "Service:/ebus/addr@16/device@BASV2/method@get_operational_data",
"path": "Observability:/ebus/addr@16/device@BASV2/method@get_operational_data",
"canonicalPath": "Service:/ebus/addr@16/device@BASV2/method@get_operational_data"
}
],
"edges": [
{
"id": "Observability:Service:/ebus/addr@16/device@BASV2->Service:/ebus/addr@16/device@BASV2/method@get_operational_data",
"from": "Service:/ebus/addr@16/device@BASV2",
"to": "Service:/ebus/addr@16/device@BASV2/method@get_operational_data"
}
]
}The projection browser is a read-only projection explorer served at /ui by default. It uses a single GraphQL query to fetch projections for all devices, then renders the device list, plane picker, projection graph, and node details. The browser auto-refreshes on an interval (default 5s) and exposes manual refresh + pause/resume controls. This surface is separate from the Portal shell/API served under /portal and /portal/api/v1.
query ProjectionBrowserProjections {
devices {
address
addresses
manufacturer
deviceId
projections {
plane
nodes { id path canonicalPath }
edges { id from to }
}
}
}Notes
Projection.planeis the plane label shown in the projection browser plane picker (ordered with defaultsService,Observability,Debug,Virtual, then any device-specific planes).ProjectionNode.idis the canonical Service path for the node, so it is stable across planes within a snapshot.ProjectionNode.pathis the plane-specific path shown in the projection browser.ProjectionNode.canonicalPathis the Service-plane path used to correlate nodes across planes.
The gateway exposes a lightweight HTTP endpoint to fetch a single projection graph from the latest schema snapshot (outside GraphQL). The projection browser uses GraphQL; the snapshot endpoint is intended for lightweight or plane-specific clients. The default path is /snapshot and can be configured via -snapshot-path.
Request
- Method:
GET - Query params:
address(required): device address as decimal or hex (e.g.,16or0x10)plane(required): projection plane name (e.g.,Service,Observability,Debug)
GET /snapshot?address=0x10&plane=Observability
Accept: application/json
{
"address": 16,
"plane": "Observability",
"nodes": [
{
"id": "Service:/ebus/addr@16/device@BASV2",
"path": "Observability:/ebus/addr@16/device@BASV2",
"canonicalPath": "Service:/ebus/addr@16/device@BASV2"
},
{
"id": "Service:/ebus/addr@16/device@BASV2/method@get_operational_data",
"path": "Observability:/ebus/addr@16/device@BASV2/method@get_operational_data",
"canonicalPath": "Service:/ebus/addr@16/device@BASV2/method@get_operational_data"
}
],
"edges": [
{
"id": "Observability:Service:/ebus/addr@16/device@BASV2->Service:/ebus/addr@16/device@BASV2/method@get_operational_data",
"from": "Service:/ebus/addr@16/device@BASV2",
"to": "Service:/ebus/addr@16/device@BASV2/method@get_operational_data"
}
]
}NewHandler(builder) returns an http.Handler backed by the query schema. cmd/gateway uses NewInvokeHandler(builder, registry, invoker) for /graphql and NewSubscriptionHandler(builder, registry, invoker, hub) for /graphql/subscriptions.
The invoke mutation validates parameters against the method signature and routes the request through the Router/Bus stack.
type Mutation {
invoke(address: Int!, plane: String!, method: String!, params: JSON): InvokeResult!
setBoilerConfig(field: String!, value: String!): BoilerConfigMutationResult!
}
type InvokeResult {
ok: Boolean!
error: InvokeError
result: JSON
}
type InvokeError {
message: String!
code: String!
category: String!
}
type BoilerConfigMutationResult {
success: Boolean!
error: String
}- Parameter validation: params are checked against either the template schema (
ParamSchema) or a template builder (Build).- Whole-number JSON numerics are accepted for integer fields (for example GraphQL-decoded
float64(2.0)andjson.Number("2")). - Fractional values for integer fields are rejected (
INVALID_PAYLOAD).
- Whole-number JSON numerics are accepted for integer fields (for example GraphQL-decoded
- Error mapping: typed errors are mapped to
code/category(e.g.,TIMEOUT→TRANSIENT,NO_SUCH_DEVICE→DEFINITIVE). - Result normalization:
types.Value{Valid:false}fields are returned asnullin the JSON result map. - Boiler writes:
setBoilerConfigsupportsflowsetHcMaxC,flowsetHwcMaxC,partloadHcKW, andpartloadHwcKW. The mutation accepts the value as a string, rejects non-finite input, enforces server-side ranges, and only reports success after B509 ack + read-back confirmation. - Boiler write normalization:
DATA2cboiler temperature writes publish the normalized wire value, not the raw input string.UCHpower-limit writes require whole-number kW.
NewInvokeHandler(builder, registry, invoker) returns an http.Handler backed by query + mutation schema.
Subscriptions deliver real-time updates over WebSocket or SSE.
type Subscription {
broadcast(primary: Int!, secondary: Int!): BroadcastEvent!
zonesUpdate: [Zone!]!
dhwUpdate: Dhw
energyTotalsUpdate: EnergyTotals
boilerStatusUpdate: BoilerStatus
systemUpdate: SystemStatus
circuitsUpdate: [CircuitStatus!]!
}
type BroadcastEvent {
source: Int!
target: Int!
primary: Int!
secondary: Int!
data: [Int!]!
}- WebSocket: supports
graphql-transport-wsand legacygraphql-wssubprotocols. - SSE fallback: request with
Accept: text/event-streamor?sse=1. Supports GET query params or POST JSON body.
NewSubscriptionHandler(builder, registry, invoker, hub) returns an http.Handler that serves both WebSocket and SSE.