Skip to content

Pattern 13: Optimistic Command Execution

Problem

When a user sends a command (e.g., Open Door), the device acts immediately but the UI waits for a device event to update status. This creates a 1-5 second lag that makes the system feel slow.

Solution

Use ExecuteCommand extension method on Thing for optimistic status updates:

  1. Sets pending status immediately (UI shows spinner)
  2. Returns Succeeded after device ACK (command handler ends fast)
  3. Confirms final status asynchronously via device event
  4. Protects pending status from poll overwrite

Two Modes

Event-Confirmed Commands

For commands where the device sends a confirmation event (Door, ControlPoint, MPG):

public static async Task<DeviceCommandResult> Open(
    DoorDevice door,
    Access.Open command,
    MyProtocol protocol,
    CancellationToken ct)
{
    return await door.ExecuteCommand(
        DoorState.UnsecuredClosed,
        send: async () =>
        {
            var reply = await protocol.SendOpenCommand(door.Address, ct);
            return reply.IsSuccess;
        },
        protocol,
        predicate: e => e.EventType == EventType.StrikeReleased
                      && e.SourceAddress == door.Address,
        onSuccess: async evt =>
        {
            var ts = DeviceTimestamp.FromUnixSeconds(evt.Timestamp);
            await door.UnsecuredRemote(ts, command.OperatorIdentity);
        });
}

Lifecycle:

pending → send → ACK → return Succeeded → ... → event → final + onSuccess
                                            └→ timeout (30s) → revert + onFault
         └→ ACK fail → revert + onFault → return Failed

Immediate Commands

For commands without confirmation events (AlarmInput Bypass, DoorAlarm Bypass):

public static async Task<DeviceCommandResult> Bypass(
    AlarmInputDevice ai,
    Monitoring.Bypass command,
    MyProtocol protocol,
    CancellationToken ct)
{
    return await ai.ExecuteCommand(
        SecurityState.Bypassed,
        send: async () =>
        {
            var reply = await protocol.SendBypassCommand(ai.Address, ct);
            return reply.IsSuccess;
        },
        onSuccess: async () => await ai.Bypassed(DeviceTimestamp.UtcNow, command.OperatorIdentity));
}

Lifecycle:

pending → send → ACK → final + onSuccess → return Succeeded
         └→ ACK fail → revert + onFault → return Failed

ACK-Only Commands

For commands where even an immediate final state would be misleading because the device only confirms acceptance, use a plain handler that sends the command and returns the ACK/NACK result. Do not manufacture a PQ event or pending/final state just to satisfy the optimistic command pattern.

public static async Task<DeviceCommandResult> Bypass(
    AlarmInputDevice ai,
    Monitoring.Bypass command,
    MyProtocol protocol,
    CancellationToken ct)
{
    var accepted = await protocol.SendBypassCommand(ai.Address, ct);
    return accepted
        ? DeviceCommandResult.Succeeded()
        : DeviceCommandResult.Failed("Device rejected bypass command.");
}

Document this mode in the adapter docs. A similarly named vendor event does not make the command event-confirmed unless it is reliably emitted for the remote command.

API Reference

Event-Confirmed Overload

Task<DeviceCommandResult> ExecuteCommand<TEvent, TState>(
    this Thing thing,
    TState finalState,
    Func<Task<bool>> send,
    ProtocolBase<TEvent> protocol,
    Func<TEvent, bool> predicate,
    Func<TEvent, Task>? onSuccess = null,
    Func<Task>? onFault = null,
    TimeSpan? timeout = null,        // default: 30 seconds
    CancellationToken ct = default)
    where TState : struct, Enum

Immediate Overload

Task<DeviceCommandResult> ExecuteCommand<TState>(
    this Thing thing,
    TState finalState,
    Func<Task<bool>> send,
    Func<Task>? onSuccess = null,
    Func<Task>? onFault = null,
    CancellationToken ct = default)
    where TState : struct, Enum

Parameters

Parameter Description
finalState Expected final state enum value (e.g., DoorState.UnsecuredClosed)
send Async function that sends command to device, returns true on ACK
protocol Protocol instance for event interception (event-supervised only)
predicate Filter for matching audited final execution event (event-supervised only)
onSuccess Called after final status is set (receives event for event-supervised)
onFault Called on send failure or timeout - for compensation logic
timeout Max wait for confirmation event, default 30 seconds

Status String Convention

Framework appends .pending suffix to status during command execution:

  • DoorState.UnsecuredClosed"UnsecuredClosed.pending"

UI interprets this suffix to show loading indicator. No framework stripping needed.

Poll Protection

While a function is pending, ApplyPollSnapshot excludes it from status updates. This prevents poll from overwriting optimistic status before device confirms.

When to Use

Scenario Mode Example
Device sends or exposes audited final execution event Event-supervised Door Open/Lock/Unlock, delayed Arm, ControlPoint, MPG
No confirmation event expected Immediate AlarmInput Bypass, DoorAlarm Bypass
Only command acceptance is knowable Plain ACK-only Bypass/remove-bypass commands with no reliable remote confirmation
Status unknown after command Neither Use standard command handling

Error Handling

  • Send fails: Reverts to original status, calls onFault, returns Failed
  • Event timeout: Reverts to original status, calls onFault (fire-and-forget)
  • Exception in callback: finally { EndPending() } ensures cleanup

Forbidden Pattern

Do not implement state-changing commands by manually waiting for the final state event before returning DeviceCommandResult. This mixes command acceptance with final execution and breaks delayed actions. New adapters must use ExecuteCommand for event-supervised commands from the start.

The event-supervision predicate must target audited device events/log entries, not ordinary status-change callbacks or polling snapshots, unless no audited event exists and the adapter documents that fallback.