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

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