Functions¶
A Function represents a specific capability of a Thing - what it can do and what state it reports.
What is a Function?¶
+--------------------------------------------------+
| Door (Thing) |
+--------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | DoorController | | ContactSensor | |
| | (Function) | | (Function) | |
| +----------------+ +----------------+ |
| | States: | | States: | |
| | - locked | | - open | |
| | - unlocked | | - closed | |
| +----------------+ +----------------+ |
| |
+--------------------------------------------------+
Functions are:
- Capabilities - what a Thing can do
- State machines - have defined states and transitions
- Status sources - report current state to PQ
- Event sources - publish events when state changes
Function vs Thing¶
| Aspect | Thing | Function |
|---|---|---|
| Represents | Physical/logical device | Capability of device |
| Hierarchy | Parent-child tree | Belongs to one Thing |
| Identity | Has DeviceId | Has FunctionId |
| Examples | Door, Reader, Panel | DoorController, CardReader, AccessController |
Thing: "This is a door"
Function: "This door can lock/unlock" (DoorController)
Function: "This door detects open/closed" (ContactSensor)
StatusPoll¶
For devices that require polling (no real-time events), the framework provides automatic status polling via PollStatus handlers and StatusBatch. Write a static PollStatus method, and the source generator wires it into the framework scheduler - no YAML config needed.
See Pattern 12: Status Polling for full details.
Common Function Types¶
Access Control¶
Door
- Physical door with strike + deadbolt + position
- States: secured_closed, unsecured_open, locked, forcedopen, etc.
- Events: door.secured, door.opened, door.forced
- Actions: Secured(), Unsecured(), Locked(), Opened(), ForcedOpen()
Reader
- Credential reader (card, PIN, biometric, mobile)
- States: reader.lockout, reader.duress
- Events: access.granted, access.denied, credential.unknown
- Actions: DuressCodeEntered(), LockoutTriggered()
Intrusion Detection¶
IntrusionDetector
- Generic intrusion sensor (PIR, contact, glass break, shock, vibration)
- States: normal, alarm, prealarm, tampered, bypassed, motiondetected, opened
- Events: intrusion.alarm, intrusion.bypassed, intrusion.motion.detected, intrusion.contact.open
- Actions: Alarm(), Bypassed(), MotionDetected(), ContactOpen(), ForcedEntry()
Partition
- Logical alarm group/area containing detectors
- States: armed, disarmed, armedinstant, partialarmed, entrydelay, exitdelay, alarm
- Events: intrusion.armed, intrusion.disarmed, intrusion.alarm, intrusion.duress
- Actions: Armed(), Disarmed(), ArmedInstant(), AlarmTriggered(), DuressDetected()
Hardware Monitoring¶
ConnectionBase
- Device communication lifecycle
- States: online, offline, degraded
- Events: connection.connected, connection.disconnected
AuthenticatedProtocol
- Protocol authentication / secure-session health, separate from link reachability
- States: protocol.authentication.failed, protocol.handshake.failed
- Actions: SetAuthenticated(), SetAuthenticationFailed(), SetHandshakeFailed(), SetDefault()
Power
- Power supply and battery monitoring
- States: mainspowered, batterybackup, lowbattery, criticalbattery
- Events: supervision.power.failure, supervision.power.restored, supervision.battery.low
Tamper
- Physical tamper detection
- States: tampered, tamper.sealed
- Events: intrusion.tamper
Generic I/O¶
Input
- Generic monitored input point
- States: normal, alarm, fault
- Events: detection.active, detection.clear
Output
- Generic output control point
- States: on, off
- Events: control.activated, control.deactivated
Function Structure¶
public abstract class FunctionBase<TStatus> where TStatus : Enum
{
// owner Thing
public Thing Owner { get; }
// function identifier (within Thing)
public string FunctionId { get; }
// current state
public TStatus CurrentState { get; protected set; }
// publish state change
protected void SetStatus(TStatus newState);
// publish event
protected void PublishEvent(string eventType, object data);
}
Function State Machine¶
+----------+
Lock() | | Unlock()
+------------>| locked |<--------------+
| | | |
| +----+-----+ |
| | |
| | UnlockMomentary() |
| v |
+-----+------+ +----------+ +----------+-----+
| | | | | |
| unlocked |<----| unlocking|--->| auto-relocking |
| | | | | |
+------------+ +----------+ +----------------+
|
| (timeout)
v
+----------+
| locked |
+----------+
Calling Function Actions¶
Each action a function declares in functions.yaml becomes a method on the Thing. One call sets the function state and publishes the action's events. The adapter only calls the method with the device timestamp:
// generated on AcmeDoor — do not write this yourself
public ValueTask<bool> UnlockedRemote(DeviceTimestamp timestamp, Guid? personId = null) =>
_doorFunction.UnlockedRemote(timestamp, personId);
// device reports: operator unlocked the door from the panel
await door.UnlockedRemote(timestamp, personId: operatorId); // state lock.secured + pq.event.access.door.unlocked.remote
// device reports: deadbolt engaged
await door.Locked(timestamp); // state lock.deadbolt (status-only action, no event)
// device reports: door opened
await door.Opened(timestamp); // pq.event.access.door.opened
An event that no function action publishes is listed under the device type's events: and gets its own generated method — see Declared Event Methods. Behaviour a framework function does not cover goes into a custom function — see Pattern 05: Custom Functions.
Status Aggregation¶
Thing collects status from all its functions:
Door (Thing)
|
+-- DoorController: "locked"
+-- ContactSensor: "closed"
+-- Communicator: "online"
Aggregated status published to PQ:
{
"deviceId": "door-1",
"functions": {
"door-controller": "locked",
"contact-sensor": "closed",
"communicator": "online"
}
}
// in Thing class
public void OnFunctionStatusChanged(string functionId, string status)
{
// aggregate all function statuses
var allStatuses = _statusAggregator.SetStatus(functionId, status);
// publish combined status
PublishStatus(new StatusMessage(
adapterId: GetAdapter().DeviceId,
nodeId: this.DeviceId,
data: allStatuses));
}
Functions in YAML¶
Define functions in adapter-registration.yaml:
device_types:
- type_id: Door
functions:
- DoorController # lock/unlock capability
- ContactSensor # open/closed detection
- type_id: Reader
functions:
- CardReader # card reading
- Communicator # connection status
Framework spec functions.yaml defines available functions:
functions:
- id: DoorController
states: [locked, unlocked, unknown]
events: [door.locked, door.unlocked]
actions: [lock, unlock, unlock_momentary]
- id: ContactSensor
states: [open, closed, unknown]
events: [door.opened, door.closed]
Function Events¶
Functions are primary source of events:
Device event occurs
|
v
+------------------+
| Protocol Layer |
| receives event |
+------------------+
|
v
+------------------+
| Find Function |
| on target Thing |
+------------------+
|
v
+------------------+
| Function.OnXxx() |
| updates state |
| publishes event |
+------------------+
|
v
+------------------+
| Event flows to |
| PQ via NATS |
+------------------+
// protocol receives device event
private void OnDeviceAccessLog(AccessLog log)
{
// find the door Thing
var door = FindDoor(log.DoorId);
// get its AccessController function
var accessController = door.GetFunction<AccessController>();
// function handles event
accessController.OnAccessEvent(
personId: log.UserId,
granted: log.Result == AccessResult.Granted,
timestamp: log.Timestamp);
}
Multiple Functions per Thing¶
public class Door : Thing
{
public DoorController LockControl { get; }
public ContactSensor Contact { get; }
public Communicator Connection { get; }
public Door(Thing parent, Guid id) : base(parent, id)
{
LockControl = new DoorController(this);
Contact = new ContactSensor(this);
Connection = new Communicator(this);
}
}
Each function:
- Has its own state
- Publishes its own events
- Contributes to Thing's aggregated status
Function Discovery¶
Get function from Thing:
// by type
var doorController = door.GetFunction<DoorController>();
// by id
var function = door.GetFunction("door-controller");
// all functions
foreach (var func in door.Functions)
{
Console.WriteLine($"{func.FunctionId}: {func.CurrentState}");
}
Common Mistakes¶
1. Status Without Event¶
// WRONG - state changed but no event
CurrentState = DoorLockState.Unlocked;
// RIGHT - the function action sets state and publishes the event in one call
await door.UnlockedRemote(timestamp);
2. Event Without Status¶
Building a function's event by hand publishes it but leaves the function state untouched — the door still reports "locked". The build reports it as PQC203.
// RIGHT - call the function action; it updates state and publishes the event
await door.UnlockedRemote(timestamp);
3. Wrong Thing Owns Function¶
// WRONG - function on wrong Thing
var cardReader = new CardReader(adapter); // should be on reader Thing
// RIGHT
var readerThing = new Reader(door);
var cardReader = new CardReader(readerThing);
Next: Protocol - Device communication layer