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:
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
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.