Commands¶
A command is a request from PQ system asking your adapter to do something.
What is a Command?¶
graph LR
PQ["PQ System (user clicks unlock)"] -->|"Unlock door 5"| Adapter["Your Adapter (executes on device)"]
Commands are:
- Requests - PQ asks, your adapter does
- Future tense - "please do this"
- Your responsibility - you write code to handle them
Available Commands¶
Commands are defined in pq-commands.yaml. Common ones:
| Command | What PQ wants |
|---|---|
pq.command.access.open |
Unlock/open access point momentarily |
pq.command.access.lock |
Lock access point |
pq.command.access.unlock |
Unlock access point (stay unlocked) |
pq.command.access.synchronize |
Sync credentials to device |
pq.command.intrusion.arm |
Arm intrusion zone |
pq.command.intrusion.disarm |
Disarm intrusion zone |
Your adapter declares which commands it supports in adapter-registration.yaml:
Writing a Command Handler¶
When PQ sends a command, framework calls your handler. You write a method with specific signature.
The Signature¶
public Task<DeviceCommandResult> MethodName(
YourThingType thing, // required: the device to act on
CommandType command, // required: the command
...) // optional: anything you need from DI
- Return type:
Task<DeviceCommandResult>orValueTask<DeviceCommandResult> - First parameter: Your Thing type (the target device) - required
- Second parameter: Command type (e.g.,
Access.Open) - required - Additional parameters: Optional - resolved from DI container or
CancellationToken
Example: Minimal Handler¶
public class DoorCommandHandler
{
public static Task<DeviceCommandResult> OpenDoor(
MyDoor door,
Access.Open command)
{
// just Thing + Command, no extras
door.Unlock();
return Task.FromResult(new DeviceCommandResult(CommandResult.Success));
}
}
Example: Handler with Dependencies¶
public class DoorCommandHandler
{
public static async Task<DeviceCommandResult> OpenDoor(
MyDoor door,
Access.Open command,
Protocol protocol) // <-- your class, resolved from DI
{
await protocol.UnlockDoor(door.Address, duration: 5);
return new DeviceCommandResult(CommandResult.Success);
}
}
What is Protocol? Your class that communicates with the physical device. See Protocol concept for details.
You can request any registered service as a parameter:
public static async Task<DeviceCommandResult> OpenDoor(
MyDoor door,
Access.Open command,
Protocol protocol, // your protocol class
ILogger<DoorCommandHandler> logger, // framework logging
CancellationToken cancellationToken) // cancellation support
{
...
}
Framework automatically:
- Finds your handler by matching
MyDoor+Access.Opentypes - Resolves additional parameters from DI
- Calls your method when command arrives
Handler Patterns¶
Three ways to write handlers:
1. Static Method (Recommended)¶
public class DoorCommands
{
public static async Task<DeviceCommandResult> Open(
MyDoor door,
Access.Open command,
Protocol protocol)
{
await protocol.UnlockDoor(door.Address);
return new DeviceCommandResult(CommandResult.Success);
}
public static async Task<DeviceCommandResult> Lock(
MyDoor door,
Access.Lock command,
Protocol protocol)
{
await protocol.LockDoor(door.Address);
return new DeviceCommandResult(CommandResult.Success);
}
}
Simple, no state, dependencies injected as parameters.
2. Instance Method¶
public class DoorCommandHandler
{
private readonly ILogger<DoorCommandHandler> _logger;
public DoorCommandHandler(ILogger<DoorCommandHandler> logger)
{
_logger = logger;
}
public async Task<DeviceCommandResult> Handle(
MyDoor door,
Access.Open command,
Protocol protocol)
{
_logger.LogInformation("Opening door {Address}", door.Address);
await protocol.UnlockDoor(door.Address);
return new DeviceCommandResult(CommandResult.Success);
}
}
Framework creates instance via DI, resolves constructor dependencies.
3. Self-Handler (on Thing itself)¶
public partial class MyDoor : Thing
{
public async Task<DeviceCommandResult> Handle(
Access.Open command,
Protocol protocol)
{
// 'this' is the door
await protocol.UnlockDoor(this.Address);
return new DeviceCommandResult(CommandResult.Success);
}
}
Handler lives on the Thing class itself. No separate Thing parameter needed.
Command Types¶
Commands live in Pq.Adapters.Framework.Commands namespace, organized by category:
using Pq.Adapters.Framework.Commands;
// Access commands
Access.Open
Access.Lock
Access.Unlock
Access.Synchronize
// Intrusion commands
Intrusion.Arm
Intrusion.Disarm
Use them in handler signatures:
Each command has OperatorIdentity property - the user who triggered it.
Returning Results¶
// success
return DeviceCommandResult.Succeeded();
// failure
return DeviceCommandResult.Failed();
// timeout
return new DeviceCommandResult(CommandResult.Timeout);
Handlers should not return CommandResult.NotFound; the dispatcher uses it when the target device or command handler cannot be found.
Accessing Command Data¶
Some commands carry additional data:
public async Task<DeviceCommandResult> Handle(
MyDoor door,
Access.Open command)
{
// who triggered this command
var operatorId = command.OperatorIdentity;
await _protocol.UnlockDoor(door.Address);
return new DeviceCommandResult(CommandResult.Success);
}
Complete Example¶
// adapter-registration.yaml declares: pq.command.access.open
public class DoorCommands
{
public static async Task<DeviceCommandResult> OpenDoor(
DoorDevice door,
Access.Open command,
Protocol protocol,
ILogger<DoorCommands> logger)
{
logger.LogInformation(
"Opening door {DoorId} requested by {Operator}",
door.DeviceId,
command.OperatorIdentity);
try
{
await protocol.UnlockDoor(door.Address, duration: 5);
return new DeviceCommandResult(CommandResult.Success);
}
catch (DeviceOfflineException)
{
logger.LogWarning("Door {DoorId} is offline", door.DeviceId);
return new DeviceCommandResult(CommandResult.Failure);
}
}
}
ExecuteCommand Pattern¶
For commands that update status, use ExecuteCommand extension method. It handles:
- Optimistic pending status (UI shows "in progress")
- ACK handling
- Event confirmation (optional)
- Automatic revert on failure
Immediate Command (ACK only)¶
Device acknowledges immediately, no follow-up event:
public async Task<DeviceCommandResult> Handle(
MyDoor door,
Access.Lock command,
Protocol protocol)
{
return await door.ExecuteCommand(
pendingState: DoorState.Secured,
send: () => protocol.SendLock(door.Address),
onSuccess: () => door.SecuredRemote(DeviceTimestamp.UtcNow, command.OperatorIdentity));
}
Flow:
- Set
secured.pendingstatus - Send command, wait for ACK
- On ACK: call
onSuccess, returnCommandResult.Success - On failure: revert status, return
CommandResult.Failure
Event-Supervised Command¶
Device ACKs command, then writes/emits an audited event when action completes:
public async Task<DeviceCommandResult> Handle(
MyDoor door,
Access.Open command,
Protocol protocol)
{
return await door.ExecuteCommand(
pendingState: DoorState.Unsecured,
send: () => protocol.SendOpen(door.Address),
protocol: protocol,
predicate: e => e.Type == EventType.StrikeReleased && e.DoorId == door.Address,
onSuccess: evt => door.UnsecuredRemote(evt.Timestamp, command.OperatorIdentity));
}
Flow:
- Set
unsecured.pendingstatus - Register event intercept (before send to avoid race)
- Send command, wait for ACK
- On ACK: return
Succeededimmediately (command accepted by device/control system) - Background: wait for audited execution event
- On event: call
onSuccesswith event data - On timeout: revert status, call
onFault
DeviceCommandResult reports command acceptance, not final physical execution. This matters for delayed actions such as intrusion arming with an exit-delay timer: the command can be accepted now, while the audited Armed event may arrive minutes later. ExecuteCommand keeps the Thing pending and supervises that later execution event.
The predicate should match the protocol's audited event/log entry for the action. Do not use ordinary status-change or polling snapshots as the pairing source when the protocol provides audited events; status callbacks are a documented fallback only when no audited event exists.
sequenceDiagram
participant UI
participant Thing
participant Protocol
participant Device
UI->>Thing: Command
Thing->>Thing: Set pending status
Thing->>Protocol: Register intercept
Thing->>Device: Send command
Device-->>Thing: ACK
Thing-->>UI: Succeeded (optimistic)
Note over Device: Action completes
Device->>Thing: Confirming event
Thing->>Thing: Set final status
Summary¶
- Declare supported commands in
adapter-registration.yaml - Write handler method with signature:
Thing + Command -> DeviceCommandResult - Add DI parameters as needed
- Use
ExecuteCommandfor status-updating commands - Framework routes commands to your handlers automatically
Next: Properties - Device configuration