Skip to content

Pattern 2: Event Translation

Event translation maps vendor events to generated methods on the Thing — function actions or declared event methods — or to intentional unknown/unresolved handling.

Preferred Flow

Vendor event -> Protocol/Event stream -> EventRouter -> generated method on the Thing -> NATS/audit/status

Use function methods when the framework has a matching function action. Function methods update status and publish the mapped event.

router.When(e => e.Type == VendorEventType.DoorOpened)
    .WithAddress<DoorThing, int>(e => e.DoorId)
    .Handle(async (door, e) =>
    {
        var ts = DeviceTimestamp.FromUnixSeconds(e.Timestamp);
        await door.Opened(ts);
        return true;
    });

router.When(e => e.Type == VendorEventType.ZoneAlarm)
    .WithAddress<ZoneThing, int>(e => e.ZoneId)
    .Handle(async (zone, e) =>
    {
        await zone.Alarm(DeviceTimestamp.UtcNow);
        return true;
    });

Function actions also carry identity. Reader publishes pq.event.access.granted through AccessGranted:

router.When(e => e.Type == VendorEventType.AccessGranted)
    .WithAddress<ReaderThing, int>(e => e.ReaderId)
    .WithPerson(e => e.UserId.ToString())
    .WithCredential(e => e.CardNumber)
    .Handle(async (reader, personId, credentialId, e) =>
    {
        var ts = DeviceTimestamp.FromUnixSeconds(e.Timestamp);
        await reader.AccessGranted(ts, credentialId: credentialId, personId: personId);
        return true;
    });

Declared Event Methods

When no function action publishes the event, list the event id under the device type's events: in adapter-registration.yaml. The generator puts a named method on the Thing; the router calls it:

device_types:
  - type_id: "Panel"
    events:
      - pq.event.technical.fault.hardware
router.When(e => e.Type == VendorEventType.LicenceRemoved)
    .WithAddress<Panel, int>(e => e.PanelId)
    .Handle(async (panel, e) =>
    {
        await panel.HardwareFault(DeviceTimestamp.FromUnixSeconds(e.Timestamp), faultDescription: "Device licence removed");
        return true;
    });

The method parameters come from the taxonomy: required ones plain, optional ones nullable with = null. Adapter code never references PqEvent or calls EventBuilder — the build reports both as PQC203. Naming and parameter rules are in Declared Event Methods.

Unknown And Unresolved Events

Do not silently drop relevant vendor events.

router.Unhandled(async (owner, e, reason, ct) =>
{
    await owner.Unknown(
        DeviceTimestamp.UtcNow,
        $"vendor:{e.Type}:{e.SubType}",
        reason,
        cancellationToken: ct);
});

Control-plane noise such as keepalive, ACK, or expected session maintenance should be handled internally with debug logging instead of audited as unknown.

Rules

  • Every vendor event category must be mapped, intentionally suppressed, or recorded as unknown/unresolved.
  • Return true when a route handled the event.
  • Return false when a matching route declines an unsupported subcase and should fall through.
  • Preserve unresolved-address diagnostics; do not mask configuration drift with a broad fallback route.
  • Use DeviceTimestamp from the device payload when available; otherwise use DeviceTimestamp.UtcNow.