Skip to content

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:

protected override AccessModel.AccessModel Transform(PersonAccess[] persons)

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:

protected override AccessModel.AccessModel Transform(AccessModel.PersonAccess[] persons)

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:

public sealed record DeviceAccess(Thing Device, WeeklyTimeRange[]? 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:

person_profiles:
  - slot_type: User
    fields:
      - name: user_level
        type: int
        default: 0
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:

await node.Disconnected(timestamp);
await node.Reconnected(timestamp);

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.

device_types:
  - type_id: "AcmePanel"
    events:
      - pq.event.system.configuration.changed
// 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:

  1. Update internal function state
  2. Publish appropriate events
  3. 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

await device.TamperDetected(timestamp);
await device.TamperCleared(timestamp);

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