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
truewhen a route handled the event. - Return
falsewhen 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
DeviceTimestampfrom the device payload when available; otherwise useDeviceTimestamp.UtcNow.