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
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 |
+----------+
Implementing a Function¶
public enum DoorLockState
{
Unknown,
Locked,
Unlocked
}
public class DoorController : FunctionBase<DoorLockState>
{
public DoorController(Thing owner)
: base(owner, "door-controller")
{
CurrentState = DoorLockState.Unknown;
}
public void Lock()
{
// ... send command to device ...
SetStatus(DoorLockState.Locked);
PublishEvent(
PqEvent.Access.Door.Locked
.At(DeviceTimestamp.UtcNow, Owner)
.Build());
}
public void Unlock()
{
// ... send command to device ...
SetStatus(DoorLockState.Unlocked);
PublishEvent(
PqEvent.Access.Door.Unlocked
.At(DeviceTimestamp.UtcNow, Owner)
.Build());
}
public async Task UnlockMomentary(int durationSeconds)
{
Unlock();
await Task.Delay(TimeSpan.FromSeconds(durationSeconds));
Lock();
}
// called when device reports state change
public void OnDeviceStateChanged(bool isLocked)
{
var newState = isLocked ? DoorLockState.Locked : DoorLockState.Unlocked;
if (newState != CurrentState)
{
SetStatus(newState);
var eventBuilder = isLocked
? PqEvent.Access.Door.Locked.At(DeviceTimestamp.UtcNow, Owner)
: PqEvent.Access.Door.Unlocked.At(DeviceTimestamp.UtcNow, Owner);
PublishEvent(
eventBuilder
.WithParameter("source", "device") // device reported, not command
.Build());
}
}
}
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 - always pair with event
SetStatus(DoorLockState.Unlocked);
PublishEvent(
PqEvent.Access.Door.Unlocked
.At(timestamp, door)
.Build());
2. Event Without Status¶
// WRONG - event published but state not updated
PublishEvent(
PqEvent.Access.Door.Unlocked
.At(timestamp, door)
.Build());
// CurrentState still shows "locked"
// RIGHT - update state first
SetStatus(DoorLockState.Unlocked);
PublishEvent(
PqEvent.Access.Door.Unlocked
.At(timestamp, door)
.Build());
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