Pattern 12: Status Polling¶
When to Use¶
Polling fills the gap when events alone cannot guarantee fresh state.
| Scenario | Use polling? |
|---|---|
| Device only supports polling (no events) | Yes |
| Device has events but may miss some on reconnect | Yes - fill gaps |
| Need initial state on adapter start | Yes - first poll fills unknowns |
| Device has reliable real-time events | No - events are sufficient |
PollStatus Static Method¶
Preferred path: write a dedicated status polling class with a static PollStatus method. The source generator detects it by signature and wires it automatically - no interface implementation, no DI registration, and no YAML declaration required for the polling logic.
Declare StatusPoll in adapter-registration.yaml only when the Thing needs polling configuration, such as a non-default interval. Keep the polling code in the standalone class.
internal static class PanelStatusPolling
{
public static async Task PollStatus(
Panel panel, // Thing type - first param
IThingQuery.IDescendants<PartitionDevice> parts, // DI - descendant query
IThingQuery.IDescendants<ZoneDevice> zones, // DI - descendant query
Protocol protocol, // DI - adapter service
CancellationToken ct)
{
var partitionStates = await protocol.QueryPartitionStatus(ct);
foreach (var (num, armState) in partitionStates)
{
var partition = parts.FirstOrDefault(p => p.Address == num);
if (partition is null) continue;
var batch = partition.StatusBatch();
batch.Set(armState switch
{
ArmingStatus.Armed => PartitionState.Armed,
ArmingStatus.ArmedHome => PartitionState.ArmedHome,
_ => PartitionState.Disarmed,
});
await batch.Commit(ct);
}
var zoneStates = await protocol.QueryZoneStatus(ct);
foreach (var (num, flags) in zoneStates)
{
var zone = zones.FirstOrDefault(z => z.Address == num);
if (zone is null) continue;
var batch = zone.StatusBatch();
if (flags.HasFlag(ZoneFlags.Alarm)) batch.Set(IntrusionDetectorState.Alarm);
if (flags.HasFlag(ZoneFlags.Tamper)) batch.Set(TamperState.Tampered);
if (flags.HasFlag(ZoneFlags.Fault)) batch.Set(IntrusionDetectorState.Fault);
await batch.Commit(ct);
}
}
}
Detection rules:
- Method must be
public staticwith return typeTask; the containing class can beinternal static - Method name must be
PollStatus - First parameter is the Thing type that owns the poll
- Remaining parameters are resolved via DI (same as command handlers)
- Source generator emits
IPollableStatuson the Thing type and registers it with the scheduler
Use manual IPollableStatus only when the poller must keep adapter-specific state or combine polling with event backlog/replay semantics that do not fit the generated wrapper. Document why the generated static path is insufficient — and declare StatusPoll: on the device type in adapter-registration.yaml, otherwise the scheduler never registers the Thing.
Cover All Relevant States¶
The poll must read every status-bearing function the protocol exposes for each polled element in its domain, not just one state (RULE-051). A partition carries armed/disarmed (often a single bit), alarm, and trouble; a zone carries alarm, tamper, bypass, fault; a door carries its own set (open/closed, locked, forced, held); modules carry comm, tamper, power. The authoritative checklist per Thing type: the functions declared in adapter-registration.yaml intersected with the protocol's status queries — each must appear in a batch.Set(...). Skipping a whole element type (e.g. polling zones and outputs but not partitions) leaves its states permanently unknown.
StatusBatch API¶
StatusBatch collects state updates and publishes them as a single diff.
// create a batch on the Thing
var batch = thing.StatusBatch();
// set state via the state enum - its type selects the function
batch.Set(PartitionState.Armed);
batch.Set(IntrusionDetectorState.Alarm);
batch.Set(OutputState.Active);
// commit: diff against current state, one NATS publish if anything changed
await batch.Commit(ct);
Set(state) selects the target function from the state enum's type at compile time - PartitionState updates the Partition function, OutputState the Output function. There is no function argument and no scope argument.
Commit calls FunctionStatusAggregator.ApplyPollSnapshot which:
- Scopes to exactly the functions you called
Set(...)on - Diffs those against last-observed values
- Publishes one NATS message when anything changed
- No-ops when nothing changed
Functions you did not Set are left untouched — sibling functions are never cleared.
State Enum Naming¶
State enums live in Pq.Adapters.Framework and follow the pattern {Function}State:
| Function | State enum | Example values |
|---|---|---|
Partition |
PartitionState |
Armed, ArmedHome, Disarmed |
IntrusionDetector |
IntrusionDetectorState |
Alarm, Fault, Normal |
Output |
OutputState |
Active, Inactive |
ContactSensor |
ContactState |
Open, Closed |
Tamper |
TamperState |
Tampered, Normal |
Framework-Managed Functions¶
These stay framework-owned. A poll batch only touches the functions you Set(...), so it never disturbs them:
ConnectionFunction/ConnectionBaseFunctionConnectionQualityFunctionLifecycleFunctionEnrollmentFunctionAccessSynchronizationFunctionTimeSynchronizationFunctionStatusPollFunction
Scheduling¶
StatusPollScheduler is a framework singleton:
- 1-second
PeriodicTimertick - Resolves Things from
DeviceTreeRegistryat execution time - no direct references held - Only polls Things in connected state (via
IConnectionAwarelifecycle) - Default interval: 30 seconds, configurable per device type in YAML
Key Files¶
| File | Purpose |
|---|---|
Pq.Adapters.Framework/functions.yaml |
StatusPoll function definition |
Pq.Adapters.Framework/Capabilities/StatusPoll/ |
IPollableStatus, StatusPollScheduler |
Pq.Adapters.Framework/Functions/StatusPoll/StatusPollFunction.cs |
Framework function (IConnectionAware) |
Pq.Adapters.Framework/Models/StatusBatch.cs |
Batch status composition |
Pq.Adapters.Framework/Models/Functions/FunctionBase.cs |
MapToStatusString, FunctionId |
Pq.Adapters.Framework/Models/FunctionStatusAggregator.cs |
ApplyPollSnapshot diff logic |
SourceGenerator/.../StatusPollDetector.cs |
Detects PollStatus methods |
SourceGenerator/.../StatusPollDispatcherGenerator.cs |
Generates IPollableStatus on Thing types |
See Also¶
- Pattern 3: Connection Management - StatusPoll respects connection lifecycle
- Pattern 9: Event Publishing - events vs polling for status updates
- Concepts: Functions - function types and status aggregation