Skip to content

Pattern 2: Event Translation

Event translation maps vendor events to PQ framework function methods, generated PqEvent builders, or intentional unknown/unresolved handling.

Preferred Flow

Vendor event -> Protocol/Event stream -> EventRouter -> Thing/function method or PqEvent -> 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;
    });

PqEvent Builder

Use generated events or EventBuilder only when no function method represents the event.

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 evt = PqEvent.Access.Granted
            .At(DeviceTimestamp.FromUnixSeconds(e.Timestamp), reader)
            .WithIdentity(personId, credentialId)
            .Build();

        await reader.PublishEvent(evt);
        return true;
    });

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.