Events¶
An event is a message that reports something that already happened on a device.
What is an Event?¶
+------------------+ +------------------+ +------------------+
| Physical World | ---> | Adapter | ---> | PQ System |
+------------------+ +------------------+ +------------------+
"Door opened" Detects change Receives event
(something happened) Creates EventMessage Logs, notifies,
Publishes to NATS triggers rules
Events are:
- Past tense - they report what already happened
- Immutable - you cannot change or undo an event
- One-way - adapter sends, PQ receives (no response expected)
- Historical - they become part of audit trail
Event Processing Approaches¶
Modern adapters can handle events using two approaches:
Pattern 2: Manual Translation¶
Traditional approach where you manually route events in Protocol.OnEventReceived():
protected override async Task OnEventReceived(DeviceEvent evt)
{
var door = FindByAddress(evt.DoorId) as DoorThing;
if (door is null) return;
switch (evt.Code)
{
case 0x1000: // Access Granted
await door.PublishAccessGranted(...);
break;
// ... more cases
}
}
Best for:
- Simple devices with 5-10 event types
- Flat addressing structure
- Single Thing type handles all events
See Pattern 2: Event Translation for details.
EventRouter: Declarative Routing¶
Declarative approach using routing rules for automatic event dispatch:
public void Configure(IEventRoute<DeviceEvent> router)
{
router
.When(e => e.Code == 0x1000)
.WithAddress<DoorThing, uint>(e => e.DoorId)
.WithPerson(e => e.PersonId)
.WithCredential(e => e.CredentialId)
.Handle(async (door, personId, credentialId, evt) =>
{
await door.PublishAccessGranted(...);
});
}
Best for:
- Complex devices with 20+ event types
- Hierarchical addressing (Panel → SIO → Point)
- Multiple Thing types handling different events
- Identity resolution requirements
See Event Routing for details.
Classifying Device Messages¶
Not all messages from devices are events. Before processing, classify each message type:
| Message Type | Purpose | Process as Event? |
|---|---|---|
| Audit Event | Something happened (access granted, door forced) | YES - ALL |
| Status Update | Current state (door is open, device online) | NO - publish as status |
| Control Message | Protocol communication (ACK, keepalive) | NO - handle internally |
Critical: Process ALL audit events. Never silently drop events you don't recognize - publish them using the thing.Unknown() extension method. For EventRouter-based adapters, register a router.Unhandled() handler.
See Event Types for detailed classification rules.
Event vs Status vs Command¶
| I want to... | Use |
|---|---|
| Report something that happened | Event |
| Report current state | Status |
| Request an action | Command |
EVENT: "Access was granted to John at 10:15:03"
(happened, recorded, done)
STATUS: "Door is unlocked"
(current state, may change any moment)
COMMAND: "Unlock the door"
(request, not yet executed)
Common Event Types¶
Access Control Events¶
| Event | Meaning |
|---|---|
access.granted |
Someone successfully gained access |
access.denied |
Access attempt was rejected |
access.unknown |
Credential not recognized |
Door Events¶
| Event | Meaning |
|---|---|
door.opened |
Door physically opened |
door.closed |
Door physically closed |
door.forced |
Door opened without valid access |
door.held |
Door held open too long |
Device Events¶
| Event | Meaning |
|---|---|
device.online |
Device connected |
device.offline |
Device disconnected |
device.tamper |
Tamper switch triggered |
device.error |
Device reported error |
Fire Events¶
| Event | Meaning |
|---|---|
fire.alarm |
Fire alarm active |
fire.alarm.general |
General fire alarm active |
fire.alarm.section |
Section or zone-level fire alarm active |
fire.alarm.conditional |
Conditional fire alarm active |
fire.alarm.acknowledged |
Fire alarm acknowledged / kvitace |
fire.trouble |
Fire system trouble or fault |
fire.test.started |
Fire panel, group, or address test started |
fire.sounder.* |
Fire sounder/siren state changed |
fire.routing.* |
Fire alarm transmission/routing state changed |
fire.control.* |
Fire protection control output state changed |
Event Structure¶
Every event contains:
+--------------------------------------------------+
| EventMessage |
+--------------------------------------------------+
| EventType: "access.granted" |
| Severity: "info" |
| AdapterId: (which adapter sent this) |
| NodeId: (which device/thing) |
| Timestamp: 2025-01-15T10:15:03Z |
| Data: { |
| "personId": "user-123", |
| "credentialType": "card", |
| "doorName": "Main Entrance" |
| } |
+--------------------------------------------------+
Severity Levels¶
| Severity | When to use |
|---|---|
info |
Normal operation (access granted, door opened) |
warning |
Attention needed but not critical (access denied) |
error |
Something went wrong (device error, communication failure) |
critical |
Immediate attention required (forced entry, tamper) |
When Does Adapter Create Events?¶
+-------------------+
| Physical Device |
+-------------------+
|
| Device sends notification
| (gRPC stream, callback, poll result)
v
+-------------------+
| Protocol Layer |
| (your code) |
+-------------------+
|
| Transform to EventMessage
v
+-------------------+
| Publish to |
| OutboundEvent |
| Channel |
+-------------------+
|
| Framework handles rest
v
+-------------------+
| NATS -> PQ System |
+-------------------+
Your adapter:
- Receives device notification (via streaming, polling, or callback)
- Transforms device-specific format to
EventMessage - Publishes to
OutboundEventChannel
Framework handles NATS publishing automatically.
Event Data: What to Include¶
Include everything needed to understand what happened:
// GOOD - complete context
var eventData = new {
personId = "user-123", // WHO
personName = "John Smith",
credentialType = "card", // HOW
credentialId = "card-456",
doorId = "door-789", // WHERE
doorName = "Main Entrance",
timestamp = DeviceTimestamp.UtcNow, // WHEN
direction = "entry" // ADDITIONAL CONTEXT
};
// BAD - missing context
var eventData = new {
userId = "123" // who? which door? when?
};
Events You Must Implement¶
Minimum events for access control adapter:
| Event | Trigger |
|---|---|
access.granted |
Valid credential, access allowed |
access.denied |
Valid credential, access denied (no permission) |
access.unknown |
Unknown credential presented |
Recommended additional events:
| Event | Trigger |
|---|---|
door.opened |
Door contact sensor: open |
door.closed |
Door contact sensor: closed |
door.forced |
Door opened without unlock command |
door.held |
Door open longer than allowed time |
Example: Publishing an Event¶
// in your protocol layer when device reports access
private async Task OnDeviceAccessEvent(DeviceAccessLog log)
{
// transform device format to EventMessage
var eventMessage = new EventMessage(
eventType: MapAccessResult(log.Result), // "access.granted" etc.
severity: DetermineSeverity(log.Result),
adapterId: _adapterId,
nodeId: GetDoorNodeId(log.DoorId),
data: CreateEventData(log));
// publish - framework handles the rest
await _communication.PublishEventAsync(eventMessage, cancellationToken);
}
private string MapAccessResult(int deviceResult) => deviceResult switch
{
0 => "access.granted",
1 => "access.denied",
2 => "access.unknown",
_ => "access.error"
};
Events vs Polling¶
Two ways device can report events:
Push (Streaming/Callback)¶
Device ----[event]----> Adapter ----[event]----> PQ
----[event]----> ----[event]---->
----[event]----> ----[event]---->
+ Real-time
+ Efficient (no wasted requests)
- Requires persistent connection
Pull (Polling)¶
Adapter ----[request]----> Device
<---[events]------
(wait 5 seconds)
----[request]----> Device
<---[events]------
+ Works with simple REST APIs
+ No persistent connection needed
- Delay (polling interval)
- Wasted requests when no events
Your adapter must handle whichever method device supports.
Common Mistakes¶
1. Publishing Status as Event¶
// WRONG - this is status, not event
PublishEvent("door.open", ...); // door IS open (status)
// RIGHT - event reports change
PublishEvent("door.opened", ...); // door WAS opened (event)
PublishStatus("door.open", ...); // door IS open (status)
2. Missing Timestamp¶
// WRONG - when did this happen?
var eventData = new { personId = "123" };
// RIGHT - include timestamp from device
var eventData = new {
personId = "123",
timestamp = deviceEvent.Timestamp // use device time, not now
};
3. Using Current Time Instead of Device Time¶
// WRONG - uses adapter's current time
timestamp = DateTimeOffset.UtcNow
// RIGHT - uses time from device (when event actually happened)
timestamp = deviceEvent.OccurredAt
Next: Status - Reporting current state