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 - Complete event taxonomy
- pq-commands.yaml - Universal command definitions
- functions.yaml - 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 Catalog¶
Overview¶
The generator turns the event taxonomy into a nested static class catalog, PqEvent.
Each event id becomes one class:
// generated — do not write this
// pq.event.access.granted → PqEvent.Access.Granted
// pq.event.technical.fault.hardware → PqEvent.Technical.Fault.Hardware
The catalog is the generator's building material. Function actions and
declared event methods are built from it. Adapter code calls
those methods and never references PqEvent or EventBuilder — the build reports it as
PQC203.
Common Event Examples¶
Every event is published by a generated method on the Thing it happens on. A function
action publishes the events its function owns (see functions.yaml). Any other event is
listed under the type's events: in adapter-registration.yaml and gets a
declared event method.
Access Events¶
// Reader function actions
await reader.AccessGranted(timestamp, credentialId: credentialId, personId: personId);
await reader.AccessDenied(timestamp, credentialId: credentialId);
// credential not in the device database
await reader.AccessDeniedUnknown(timestamp, card_bits: bitLength, card_code: cardCode);
Door Events¶
// Door function actions
await door.Opened(timestamp); // position sensor
await door.ForcedOpen(timestamp); // alarm
await door.HeldOpenTooLong(timestamp); // held open
Power Events¶
// Power function actions
await controller.AcFault(timestamp);
await controller.BatteryLow(timestamp, batteryPercentage: batteryLevel);
Fault Events¶
# adapter-registration.yaml — declared on the panel type
events:
- pq.event.technical.fault.hardware
// declared event method
await panel.HardwareFault(timestamp, faultDescription: "Device licence removed");
Connection Events¶
The framework publishes pq.event.connection.lost and pq.event.connection.restored
for a Thing that implements IDeviceConnection. Adapter code publishes nothing for them.
A field bus sub-node with the Comm function reports its own link:
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, PqEvent or EventBuilder — see
PQC232 and PQC203 for the rules
this satisfies.
// generated on AcmePanel — do not write this
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¶
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.
Do not write the method by hand. A partial that builds the event from PqEvent is
reported as PQC203. Every shape a call needs fits the
generated method: pass a value the device reports only sometimes as null, and pass a
value the protocol implies as a constant at the call site.
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 Declared Event Methods¶
| 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 |
| State change + a separate event | Both | Shortcut for state, then the declared method for the other event |
Both kinds of method are built from the generated PqEvent catalog. Adapter code calls
the methods and never references PqEvent or EventBuilder (PQC203).
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)
await reader.AccessGranted(timestamp, credentialId: credentialId, personId: personId);
await reader.AccessDenied(timestamp, credentialId: credentialId);
// 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:
// declared event method - no state change
// (pq.event.access.granted listed under the type's events:)
await AccessGranted(evt.Timestamp, credentialId: evt.CredentialId, personId: evt.PersonId);
break;
case DeviceEventType.AccessDenied:
// (pq.event.access.denied listed under the type's events:)
await AccessDenied(evt.Timestamp, credentialId: evt.CredentialId, personId: evt.PersonId);
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 function actions vs declared event methods
- 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:
- PQC203 - Events must be declared in YAML, not built by hand
- PQC232 - Event publication must be encapsulated in the owning Thing