ADR-007: Telemetry Coalescing Groups¶
Status¶
Accepted Date: 2026-03-03
Context¶
The vito2mqtt bridge registers 7 telemetry handlers — one per signal domain (outdoor, hot_water, burner, heating_radiator, heating_floor, system, diagnosis). Each handler polls its signals from the boiler via the Optolink serial interface (ADR-003, ADR-004).
In the current cosalette execution model, each telemetry handler runs as an
independent asyncio.Task with its own sleep/execute/publish loop. When
multiple handlers share the same polling interval (6 of 7 groups default to
300 s, 1 to 3600 s per ADR-005), each opens a separate P300 serial session
at roughly the same wall-clock moment.
This causes several problems on a 4800-baud serial bus:
- Session overhead — each P300 handshake costs \~200 ms. Six separate sessions at t=300 spend \~1.2 s just on handshakes instead of \~0.2 s.
- Bus contention — rapid session cycling stresses the Vitodens controller.
- Timing drift — independent sleep loops drift apart because each handler sleeps after its own (varying-length) execution.
- Scalability — adding signal groups linearly increases session count.
The solution must work at the cosalette framework level so that other projects with shared adapter resources (serial buses, SPI interfaces, rate-limited APIs) benefit from the same mechanism.
Key requirements¶
- At t=0 (startup), all handlers fire in a single shared session.
- At coinciding ticks (e.g., t=3600 where both 300 s and 3600 s intervals fire), handlers share one session.
- Arbitrary intervals (300, 400, 550) must coalesce whenever they coincide.
- The mechanism must be a framework-level feature in cosalette.
- The user-facing API should be explicit and readable — an
group=parameter on@app.telemetry()/app.add_telemetry().
Decision¶
Add coalescing groups to cosalette's telemetry API — a new optional
group parameter on @app.telemetry() and app.add_telemetry() that declares
which handlers should share execution windows when their intervals coincide.
Handlers in the same coalescing group are managed by a shared tick-aligned scheduler that:
- Uses a priority queue to compute a global timeline of fire events
- Groups all handlers due at the same tick into a sequential batch
- Executes the batch in a single execution window (enabling adapter session sharing)
- Preserves per-handler publish strategies, error isolation, persistence, and init functions
Handlers without a group parameter (or in different groups) run independently,
preserving full backward compatibility.
User-facing API¶
# Decorator form
@app.telemetry(name="outdoor", interval=300, group="optolink")
async def poll_outdoor(port: OptolinkPort) -> dict[str, object]:
...
# Imperative form
app.add_telemetry(
name="outdoor",
func=handler,
interval=300,
group="optolink",
)
vito2mqtt usage¶
def register_telemetry(app: App) -> None:
for group_name in SIGNAL_GROUPS:
app.add_telemetry(
name=group_name,
func=_make_handler(group_name),
interval=_get_interval(settings, group_name),
publish=OnChange(),
group="optolink", # ← all share the adapter
)
Decision Drivers¶
- Minimize serial bus sessions for slow (4800 baud) Optolink interface
- Deterministic tick-aligned timing eliminates drift
- Explicit
group=parameter makes coalescing visible and intentional - Framework-level solution benefits all cosalette projects
- Backward compatible — ungrouped handlers are unaffected
- Per-handler semantics (publish strategy, error isolation, persistence) remain intact
Considered Options¶
- Option A: Tick-Aligned Scheduler (implicit, all handlers) — Replace all independent loops with a single global scheduler. All handlers are automatically coalesced.
- Option B: Adapter Keep-Alive — Keep independent handler tasks, make the adapter smart enough to hold sessions open between rapid calls.
- Option C: Coalescing Groups (explicit
group=parameter) — Users declare which handlers share execution windows via agroupparameter.
Decision Matrix¶
| Criterion | A: Global Scheduler | B: Keep-Alive | C: Coalescing Groups |
|---|---|---|---|
| Satisfies all 5 requirements | 4 | 2 | 5 |
| Framework generalizability | 5 | 1 | 4 |
| API clarity and readability | 3 | 5 | 5 |
| Implementation complexity | 3 | 3 | 3 |
| Deterministic timing (no drift) | 5 | 2 | 5 |
| Backward compatibility | 3 | 5 | 5 |
| Handles arbitrary intervals | 5 | 3 | 5 |
| Session sharing at t=0 | 5 | 2 | 5 |
| Total | 33 | 23 | 37 |
Scale: 1 (poor) to 5 (excellent)
Option C scores highest because it combines deterministic tick-aligned
scheduling with explicit user intent. The group= parameter makes the
coalescing behavior readable and intentional — developers can see at a
glance which handlers share resources, which is more valuable for long-term
maintainability than implicit "magic" scheduling.
Option A was close but penalised for implicitly changing the execution model for all handlers (reduced backward compatibility) and for hiding the coalescing intent from the reader.
Option B was rejected because it relies on timing heuristics (idle timeouts) that provide no guarantee of coalescing, especially at startup.
Consequences¶
Positive¶
- Serial sessions reduced from N (one per handler) to 1 per coinciding tick — e.g., 2 sessions per cycle instead of 7 for the default vito2mqtt config
- Deterministic tick alignment eliminates timing drift between grouped handlers
- Explicit
group=parameter is self-documenting and immediately visible in registration code - Full backward compatibility — existing ungrouped handlers work identically
- Per-handler semantics preserved: each handler retains its own publish strategy, error recovery, persistence policy, and init function
- Other cosalette projects can use coalescing groups for SPI buses, rate-limited APIs, or any shared-resource scenario
Negative¶
- New framework concept for users to learn (mitigated by being opt-in and having a clear, single-parameter API)
- Scheduler adds code complexity to cosalette's core execution path
- Within a batch, handlers execute sequentially — for adapters with independent resources this is suboptimal (mitigated: this only affects handlers that explicitly opted into the same group)
- Floating-point tick arithmetic requires care to avoid precision issues (mitigated: use integer-millisecond internal representation)
2026-03-03