Skip to content

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