Generated Code API Reference¶
The PQ Adapters Framework uses source generators to create strongly-typed APIs from YAML definitions. This eliminates manual boilerplate and ensures compile-time safety for events, commands, and function interfaces.
Framework taxonomies:
- pq-events.yaml (1143 lines) - Complete event taxonomy
- pq-commands.yaml (489 lines) - Universal command definitions
- functions.yaml (376 lines) - Standard function capabilities
See Functions Registry for detailed function reference.
Access Synchronization Generated Types¶
When an adapter contains access-model.yaml, the source generator creates access
model records, differ infrastructure, upload batch builders, and an
AccessSynchronizationBase class. The adapter implements Transform(...) in a
partial class derived from that generated base.
Standard Transform Input¶
Without person profile declarations, Transform receives framework access input:
PersonAccess contains person identity, credentials, accessible devices, and APB
flag. DeviceAccess contains the resolved Thing and the per-device schedule.
Typed Person Profile Input¶
When access-model.yaml declares person_profiles, the generator creates
adapter-specific access input in the adapter's AccessModel namespace:
Generated AccessModel.PersonAccess includes the same person data plus one typed
profile property per declared slot type:
public sealed record PersonAccess(
FrameworkPersonAccess Source,
Guid PersonId,
string Name,
CredentialEntry[] Credentials,
DeviceAccess[] Devices,
Guid? ProfileTemplateId,
VendorPanelUserProfile? UserProfile,
bool ApbExempt = false);
Generated AccessModel.DeviceAccess contains only device and schedule:
The generated mapper converts ProfileData.Fields to the typed profile before
Transform is called. Adapter code should use the typed profile property, for
example person.UserProfile, and must not parse profile JSON manually.
Generated Profile Records¶
For each person_profiles[].slot_type, the generator creates a profile record named
from the adapter and slot type, for example VendorPanelUserProfile.
Profile field definitions reuse the YAML property model. Defaults become C# property initializers:
public sealed partial record VendorPanelUserProfile
{
[JsonPropertyName("user_level")]
public int UserLevel { get; set; } = 0;
}
Required fields are checked by the generated mapper. Missing or invalid required profile data fails before adapter transformation.
PqEvent Classes¶
Overview¶
The framework generates a hierarchical class structure matching the event taxonomy. Each event becomes a nested static class with builder methods.
Example hierarchy:
// YAML: pq.event.access.granted
PqEvent.Access.Granted
// YAML: pq.event.access.door.opened
PqEvent.Access.Door.Opened
// YAML: pq.connection.connected
PqEvent.Connection.Connected
Builder Pattern¶
All PqEvent classes use fluent builder API:
PqEvent.Access.Granted
.At(timestamp, thing) // Required: when & where
.WithIdentity(personId, credId) // Optional: who & what
.WithReason("Card scan") // Optional: why
.WithParameter("key", value) // Optional: custom metadata
.Build() // Returns PqEventInstance
Builder Methods¶
.At(DeviceTimestamp timestamp, Thing thing)¶
Required. Sets event timestamp and source Thing ID from the Thing.
.WithIdentity(string? personId, string? credentialId)¶
Optional. Sets identity tracking for access control events.
var evt = PqEvent.Access.Granted
.At(timestamp, reader)
.WithIdentity(personId: "123", credentialId: "card-456")
.Build();
Both parameters are nullable - use what's available:
// Only credential known
.WithIdentity(personId: null, credentialId: cardId)
// Only person known
.WithIdentity(personId: userId, credentialId: null)
.WithReason(string reason)¶
Optional. Human-readable explanation shown in UI and logs.
.WithParameter(string key, object? value)¶
Optional. Attach custom metadata. Can be called multiple times.
var evt = PqEvent.Access.Door.Opened
.At(timestamp, door)
.WithParameter("source", "device")
.WithParameter("battery_voltage", 12.4)
.WithParameter("signal_strength", 85)
.Build();
.Build()¶
Required. Finalizes builder and returns PqEventInstance.
Common Event Examples¶
IMPORTANT: All PublishEvent examples below are called from inside Thing classes. Protocol CANNOT call PublishEvent on a Thing - see Complete Example for correct pattern.
Access Events¶
// Inside ReaderThing class - Thing publishes its own event
// Access granted
await PublishEvent(
PqEvent.Access.Granted
.At(evt.Timestamp, reader)
.WithIdentity(personId, credentialId)
.Build(),
cancellationToken
);
// Access denied
await PublishEvent(
PqEvent.Access.Denied
.At(evt.Timestamp, reader)
.WithReason("Invalid PIN")
.Build(),
cancellationToken
);
// Unknown credential
await PublishEvent(
PqEvent.Access.Credential.Unknown
.At(evt.Timestamp, reader)
.WithParameter("credential_data", rawData)
.Build(),
cancellationToken
);
Door Events¶
// Door opened (position sensor)
await PublishEvent(
PqEvent.Access.Door.Opened
.At(evt.Timestamp, door)
.Build(),
cancellationToken
);
// Door forced open (alarm)
await PublishEvent(
PqEvent.Access.Door.Forced
.At(evt.Timestamp, door)
.Build(),
cancellationToken
);
// Door held open
await PublishEvent(
PqEvent.Access.Door.HeldOpen
.At(evt.Timestamp, door)
.WithParameter("duration_seconds", 60)
.Build(),
cancellationToken
);
Power Events¶
// AC power failure
await PublishEvent(
PqEvent.Energy.Power.Failure
.At(evt.Timestamp, controller)
.Build(),
cancellationToken
);
// Battery low
await PublishEvent(
PqEvent.Technical.Battery.Low
.At(evt.Timestamp, device)
.WithParameter("battery_percentage", batteryLevel)
.Build(),
cancellationToken
);
Connection Events¶
// Device connected
await PublishEvent(
PqEvent.Connection.Connected
.At(DeviceTimestamp.UtcNow, controller)
.WithParameter("isReconnection", isReconnect)
.Build(),
cancellationToken
);
// Device disconnected
await PublishEvent(
PqEvent.Connection.Disconnected
.At(DeviceTimestamp.UtcNow, controller)
.WithReason("Network timeout")
.Build(),
cancellationToken
);
Unknown / Unresolved Events¶
Use the thing.Unknown() extension method instead of building events manually.
It automatically selects the correct PQ event type based on the UnhandledReason:
// In EventRouter Unhandled handler (preferred):
router.Unhandled(async (owner, e, reason, ct) =>
{
var ts = DeviceTimestamp.FromUnixSeconds(e.Timestamp);
await owner.Unknown(ts, $"vendor:0x{e.Code:X4}:{e.SubCode}", reason, cancellationToken: ct);
});
// In manual dispatch default case:
default:
var ts = DeviceTimestamp.FromDateTime(evt.Timestamp);
await door.Unknown(ts, $"0x{evt.Code:X4}", UnhandledReason.Unmapped,
rawData: Convert.ToBase64String(evt.RawBytes), cancellationToken: ct);
break;
Two PQ events are produced:
pq.event.device.unknown-- no route matched (unmapped vendor event)pq.event.device.unresolved-- route matched but Thing not found in device tree (includes ThingType and Address)
Declared Event Methods¶
Overview¶
Every event a device type lists under events: in adapter-registration.yaml becomes a
named publication method on that Thing. This is how a router or handler reports an
occurrence without touching PublishEvent — see
PQC232 for the rule this satisfies.
// generated on AcmePanel
public ValueTask<bool> ConfigurationChanged(
DeviceTimestamp timestamp,
string? changeDescription = null,
Guid? personId = null) =>
PublishEvent(PqEvent.System.Configuration.Changed.At(timestamp, this)
.WithParameter("changeDescription", changeDescription)
.WithParameter("personId", personId)
.Build());
The router calls the method:
private static ValueTask<bool> Route(AcmePanel panel, DeviceEvent evt) =>
panel.ConfigurationChanged(Timestamp(evt));
Method Names¶
The name is built from the fewest trailing segments of the event id — at least two — PascalCased and joined. It grows one segment at a time until it is unique among the events declared on that same type.
| Declared on the type | Generated method |
|---|---|
pq.event.system.configuration.changed |
ConfigurationChanged |
pq.event.access.door.forced |
DoorForced |
pq.event.intrusion.armed.schedule.early (alongside the next row) |
ArmedScheduleEarly |
pq.event.intrusion.disarmed.schedule.early |
DisarmedScheduleEarly |
Some events carry a recommended name in the taxonomy, used where the short derived name
would lose its meaning. That name wins: pq.event.system.firmware.update.started is
FirmwareUpdateStarted, not UpdateStarted. The recommendation is part of the
taxonomy, so it is the same for every adapter — nothing to configure.
Parameters¶
DeviceTimestamp timestamp comes first. The parameters the taxonomy declares for the
event follow — including those it inherits from its ancestors — required ones as plain
arguments, optional ones as nullable with a = null default. Required parameters come
before optional ones; within each group the order is ancestor before descendant, then
alphabetical. Every argument you pass is attached to the published event.
// version is optional on this event, so pass it only when the device reports it
await panel.FirmwareUpdateStarted(timestamp, version: reported.Version);
Supply the parameters the event carries. Dropping a value the device gave you leaves the audit record less useful than the taxonomy intends.
When a Method Is Not Generated¶
Two cases, both deliberate:
A method of that name is already written by hand. The hand-written one stands. Write a partial when the generated shape cannot express what you need — an extra parameter, a parameter supplied conditionally, a constant the protocol implies:
public partial class AcmePanel
{
/// <summary>
/// Publishes a configuration change with the operator who caused it.
/// </summary>
public ValueTask<bool> ConfigurationChanged(DeviceTimestamp timestamp, string description, Guid operatorId) =>
PublishEvent(PqEvent.System.Configuration.Changed.At(timestamp, this)
.WithParameter("changeDescription", description)
.WithParameter("personId", operatorId)
.Build());
}
An action of one of the type's functions already publishes that event. The function
action is the Thing's named surface for it and updates function state in the same call,
so call the action — door.Opened(timestamp) — and do not declare the event separately.
Inheritance¶
Methods are generated for the events declared on the type itself. A base type that declares the events gets the methods, and every type extending it inherits them. Declare a shared event once on the base rather than repeating it on each concrete type.
Thing Function Shortcuts¶
Overview¶
Functions generate shortcut methods on Thing classes for state changes. These methods automatically:
- Update internal function state
- Publish appropriate events
- Update Thing status for PQ UI
Preferred calling style: call generated function methods directly on the Thing.
await door.Opened(timestamp);
await door.LockedRemote(timestamp);
await door.UnsecuredRemote(timestamp);
// StatusBatch is the exception: the state enum's type selects the function.
batch.Set(DoorState.Secured);
When to Use Shortcuts vs PqEvent¶
| Scenario | Use | Reason |
|---|---|---|
| State change with predefined event | Function shortcut | Single call updates state + publishes event |
| Event without state change | Declared event method | Declare in events:, call the generated method — no PublishEvent in your code |
| Event whose parameters the declaration cannot express | Hand-written partial on the Thing | Your method wins over the generated one |
| State change + custom metadata | Both | Shortcut for state, then WithParameter() on any separate custom event |
The PqEvent builder is what those methods are built from. Reach for it directly only
inside the Thing's own class, when writing the partial the declaration cannot cover.
Door Function Methods¶
Door function generates methods for each action in functions.yaml:
// Strike control (access control via relay)
await door.Secured(timestamp);
await door.SecuredRemote(timestamp);
await door.Unsecured(timestamp);
await door.UnsecuredRemote(timestamp);
await door.UnsecuredByRex(timestamp);
// Deadbolt control (physical lock via motor)
await door.Locked(timestamp);
await door.LockedRemote(timestamp);
await door.Unlocked(timestamp);
await door.UnlockedRemote(timestamp);
// Position events (contact sensor)
await door.Opened(timestamp);
await door.Closed(timestamp);
// Alarm events
await door.ForcedOpen(timestamp);
await door.ForcedOpenCleared(timestamp);
await door.HeldOpenTooLong(timestamp);
await door.HeldOpenCleared(timestamp);
// Operational modes
await door.HoldUnsecured(timestamp);
// Emergency modes
await door.Evacuation(timestamp, identity);
await door.EvacuationCleared(timestamp);
await door.Lockdown(timestamp, identity);
await door.LockdownCleared(timestamp);
Inside Thing class:
protected override async Task HandleDeviceEvent(DoorEvent evt, CancellationToken ct)
{
switch (evt.Type)
{
case DoorEventType.Opened:
// Direct call, no prefix
await Opened(evt.Timestamp);
break;
case DoorEventType.Closed:
await Closed(evt.Timestamp);
break;
case DoorEventType.ForcedOpen:
await ForcedOpen(evt.Timestamp);
break;
}
}
Reader Function Methods¶
// Access events (no state change - use PqEvent instead)
// NOT GENERATED - use PqEvent.Access.Granted
// Duress
await reader.DuressCodeEntered(timestamp, identity);
await reader.DuressCleared(timestamp);
// Lockout
await reader.LockoutTriggered(timestamp, failedAttempts);
await reader.LockoutCleared(timestamp);
Power Function Methods¶
// AC power
await controller.AcFault(timestamp);
await controller.AcRestored(timestamp);
// Battery
await controller.BatteryBackup(timestamp, percentage);
await controller.BatteryFault(timestamp);
await controller.BatteryLow(timestamp, percentage);
await controller.BatteryCritical(timestamp, percentage);
await controller.BatteryNormal(timestamp);
Tamper Function Methods¶
Input/Output Function Methods¶
// Input (generic monitored point)
await input.Activated(timestamp);
await input.Deactivated(timestamp);
await input.Fault(timestamp);
// Output (generic control point)
await output.Activated(timestamp);
await output.Deactivated(timestamp);
Command Classes¶
Overview¶
Commands use hierarchical structure matching YAML naming:
YAML:
pq.command.access.lock # → Access.Lock
pq.command.output.activate # → Output.Activate
pq.command.push # → Push (top-level)
Generated classes:
namespace Pq.Adapters.Framework.Commands;
public sealed record Access
{
public sealed record Lock : IPqCommand { /* ... */ }
public sealed record Unlock : IPqCommand { /* ... */ }
public sealed record Synchronize : IPqCommand { /* ... */ }
}
public sealed record Output
{
public sealed record Activate : IPqCommand { /* ... */ }
public sealed record Deactivate : IPqCommand { /* ... */ }
}
public sealed record Push : IPqCommand { /* ... */ }
Command Handler Signature¶
Framework auto-discovers handlers via method signature - no interface required:
public class DoorThing : ThingBase
{
// Signature-based detection:
// 1. Static or instance method
// 2. Returns Task or ValueTask
// 3. Parameters: (CommandType command, CancellationToken ct)
public async Task HandleLock(Access.Lock command, CancellationToken ct)
{
// command.ThingId - target Thing
// command.UserId - who issued command
// command.Timestamp - when issued
await SendLockCommand(ct);
}
public async Task HandleUnlock(Access.Unlock command, CancellationToken ct)
{
await SendUnlockCommand(ct);
}
}
Command Properties¶
All commands include base properties:
public interface IPqCommand
{
string ThingId { get; } // Target Thing
string? UserId { get; } // Issuing user
DateTimeOffset Timestamp { get; } // Command timestamp
}
Commands with parameters add additional properties:
// pq.command.camera.preset.activate
public sealed record Camera
{
public sealed record Preset
{
public sealed record Activate : IPqCommand
{
public string ThingId { get; init; }
public string? UserId { get; init; }
public DateTimeOffset Timestamp { get; init; }
// From YAML properties
public int PresetNumber { get; init; } // range: [1, 256]
}
}
}
Handler Examples¶
// Simple command
public async Task HandlePush(Push command, CancellationToken ct)
{
await SendMomentaryPulse(ct);
}
// Command with parameters
public async Task HandlePreset(Camera.Preset.Activate command, CancellationToken ct)
{
await SendPresetCommand(command.PresetNumber, ct);
}
// Command with validation
public async Task HandleOpen(Access.Open.Permanent command, CancellationToken ct)
{
if (!SupportsExtendedAccess)
{
Logger.LogWarning("Device does not support permanent open mode");
return;
}
await SendPermanentOpenCommand(ct);
}
Function Interfaces¶
Overview¶
Framework generates interfaces for each function, allowing type-safe casting and property access:
public interface IDoorFunction
{
DoorState CurrentState { get; }
// Properties from functions.yaml
bool HasLockSensor { get; }
bool HasPositionSensor { get; }
bool HasRexSensor { get; }
int DoorlongopenTimeoutSeconds { get; }
// ... all Door properties
}
public interface IReaderFunction
{
ReaderState CurrentState { get; }
// Reader has no properties in base definition
}
public interface IPowerFunction
{
PowerState CurrentState { get; }
// Power has no properties in base definition
}
Using Interfaces¶
Cast Thing functions to interfaces when you need type-safe property access:
public class DoorThing : ThingBase
{
private readonly IDoorFunction _doorFunction;
public DoorThing(/* ... */)
{
_doorFunction = (IDoorFunction)Door; // Cast function to interface
}
private async Task CheckDoorState(CancellationToken ct)
{
if (_doorFunction.CurrentState == DoorState.SecuredClosed)
{
// Door is in normal secured state
}
if (_doorFunction.HasPositionSensor)
{
// Device reports door position via contact sensor
}
int timeout = _doorFunction.DoorlongopenTimeoutSeconds;
}
}
State Enums¶
Framework generates enums for function states:
public enum DoorState
{
// Deadbolt
Locked,
// Strike + position combined
SecuredClosed,
SecuredOpen,
UnsecuredClosed,
UnsecuredOpen,
// Extended modes
HeldUnsecured,
// Alarms
ForcedOpen,
DoorLongOpen,
// Operational
// Emergency
Lockdown,
Evacuation
}
public enum ReaderState
{
Normal,
Lockout,
Duress
}
public enum PowerState
{
MainsPowered,
BatteryBackup,
LowBattery,
CriticalBattery,
Charging,
PowerFault,
BatteryFault
}
When to Use Interfaces¶
| Scenario | Use | Example |
|---|---|---|
| State change from device | Shortcut methods | await door.Opened(timestamp) |
| Read current state | Interface cast | if (_door.CurrentState == DoorState.Locked) |
| Read properties | Interface cast | int timeout = _door.DoorlongopenTimeoutSeconds |
| Conditional logic | Interface cast | if (_door.HasPositionSensor) { ... } |
Auto-Discovery Mechanism¶
Commands¶
Framework scans for handler methods matching signature:
// Detection criteria:
// 1. Method accessibility: public
// 2. Static method, Thing self-handler, or separate instance handler
// 3. Return type: Task<DeviceCommandResult> or ValueTask<DeviceCommandResult>
// 4. Parameters:
// static/separate handler: (ThingType thing, TCommand command, ...DI services)
// Thing self-handler: (TCommand command, ...DI services)
// ✅ Valid signatures
public static Task<DeviceCommandResult> Lock(DoorThing door, Access.Lock cmd, Protocol protocol, CancellationToken ct);
public ValueTask<DeviceCommandResult> Activate(OutputThing output, Output.Activate cmd, Protocol protocol);
public Task<DeviceCommandResult> Open(Access.Open cmd, Protocol protocol); // inside DoorThing
// ❌ Invalid signatures
public void Handle(Access.Lock cmd); // wrong return type
public Task Handle(Access.Lock cmd, CancellationToken ct); // missing DeviceCommandResult
public Task<DeviceCommandResult> Handle(string command, CancellationToken ct); // wrong command type
Naming convention: Handler method name is irrelevant - detection is purely signature-based.
See Pattern 4: Command Handling for the canonical command handler rules.
Functions¶
Functions auto-register when attached to Thing:
public class DoorThing : ThingBase
{
// Framework detects these properties by type
public DoorFunction Door { get; } // Detected by name & type
public PowerFunction Power { get; } // Detected by name & type
public TamperFunction Tamper { get; } // Detected by name & type
public DoorThing(/* ... */)
{
// Functions must be initialized in constructor
Door = new DoorFunction(this, /* config */);
Power = new PowerFunction(this, /* config */);
Tamper = new TamperFunction(this, /* config */);
}
}
Complete Example¶
using Pq.Adapters.Framework;
using Pq.Adapters.Framework.Commands;
using Pq.Adapters.Framework.Events;
using Pq.Adapters.Framework.Functions;
namespace MyAdapter;
public class DoorThing : ThingBase
{
private readonly IDoorFunction _doorFunc;
public DoorFunction Door { get; }
public PowerFunction Power { get; }
public DoorThing(string thingId, IServiceProvider services)
: base(thingId, services)
{
Door = new DoorFunction(this, new DoorConfig
{
HasPositionSensor = true,
HasLockSensor = false,
DoorlongopenTimeoutSeconds = 30
});
Power = new PowerFunction(this, new PowerConfig());
_doorFunc = (IDoorFunction)Door;
}
// === DEVICE EVENT TRANSLATION ===
protected override async Task HandleDeviceEvent(byte[] rawData, CancellationToken ct)
{
var evt = ParseDeviceEvent(rawData);
switch (evt.Type)
{
case DeviceEventType.DoorOpened:
// Function shortcut - updates state + publishes event
await Opened(evt.Timestamp);
break;
case DeviceEventType.DoorClosed:
await Closed(evt.Timestamp);
break;
case DeviceEventType.AccessGranted:
// PqEvent builder - no state change
await PublishEvent(
PqEvent.Access.Granted
.At(evt.Timestamp, this)
.WithIdentity(evt.PersonId, evt.CredentialId)
.Build(),
ct
);
break;
case DeviceEventType.AccessDenied:
await PublishEvent(
PqEvent.Access.Denied
.At(evt.Timestamp, this)
.WithReason(evt.DenyReason)
.Build(),
ct
);
break;
case DeviceEventType.PowerLoss:
await AcFault(evt.Timestamp);
break;
default:
// Catch-all for unmapped events
var ts = DeviceTimestamp.FromDateTime(evt.Timestamp);
await this.Unknown(ts, evt.RawType, UnhandledReason.Unmapped,
rawData: Convert.ToBase64String(rawData), cancellationToken: ct);
break;
}
}
// === COMMAND HANDLERS ===
public async Task HandleLock(Access.Lock command, CancellationToken ct)
{
await SendToDevice(new LockCommand(), ct);
// Framework calls Door.Secured() after successful send
await SecuredRemote(DeviceTimestamp.UtcNow);
}
public async Task HandleUnlock(Access.Unlock command, CancellationToken ct)
{
await SendToDevice(new UnlockCommand(), ct);
await UnsecuredRemote(DeviceTimestamp.UtcNow);
}
public async Task HandleOpen(Access.Open command, CancellationToken ct)
{
await SendToDevice(new MomentaryOpenCommand(), ct);
await UnsecuredRemote(DeviceTimestamp.UtcNow);
// Auto-relock after timeout
await Task.Delay(
TimeSpan.FromSeconds(_doorFunc.AccessGrantTimeoutSeconds),
ct
);
await Secured(DeviceTimestamp.UtcNow);
}
}
See Also¶
- Tutorials:
- Event Flow Walkthrough - End-to-end event processing
-
YAML Configuration - Adapter registration setup
-
Patterns:
- Event Publishing - When to use functions vs PqEvent
- Event Translation - Mapping device events
-
Command Handling - Processing commands
-
Reference:
- Functions Registry - Complete function definitions
-
Framework files:
pq-events.yaml,pq-commands.yaml,functions.yaml -
Diagnostics:
- PQC232 - Event publication must be encapsulated in the owning Thing