Skip to content

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 static with return type Task; the containing class can be internal 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 IPollableStatus on 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 / ConnectionBaseFunction
  • ConnectionQualityFunction
  • LifecycleFunction
  • EnrollmentFunction
  • AccessSynchronizationFunction
  • TimeSynchronizationFunction
  • StatusPollFunction

Scheduling

StatusPollScheduler is a framework singleton:

  • 1-second PeriodicTimer tick
  • Resolves Things from DeviceTreeRegistry at execution time - no direct references held
  • Only polls Things in connected state (via IConnectionAware lifecycle)
  • Default interval: 30 seconds, configurable per device type in YAML
device_types:
  - type_id: Panel
    functions:
      StatusPoll:
        poll_interval_seconds: 10

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