External Device Id
Aggregator-backed providers (Enode, OEM cloud APIs, Tibber, …) integrate devices that already have an upstream-owned identity. The SDK records this identifier as a first-class field on the device record so the adapter can:
- Set it once at provision time, and
- Look the BT device up by it later (when an inbound webhook arrives, when a reconcile cycle runs).
Without this field, adapters end up encoding the upstream id inside the
BT deviceId (couples platform identity to the integration's choice)
or full-table-scanning to map webhooks to devices (doesn't scale).
Setting it at provision time
POST /api/devices and POST /devices/{deviceId}/provision both accept
an optional externalId:
POST /api/devices
{
"deviceId": "charger-zaptec-9b6f",
"authMethod": "symmetricKey",
"externalId": "9b6fa338-37e0-472e-9c9a-daa3471a2bc3"
}
NATS provision (bt.provider.{providerId}.admin.provision.create)
accepts the same field on its payload.
Validation:
| Rule | Value |
|---|---|
| Length | 1–256 characters |
| Charset | ASCII printable, no whitespace |
| Uniqueness | Per provider — different providers MAY use the same id |
| Mutability | Immutable — set once, never changed |
Looking it up
Adapters resolve externalId → deviceId via
IDeviceRepository.FindByExternalIdAsync — the canonical seam for
inbound paths:
public sealed class EnodeWebhookController(IDeviceRepository devices, ...)
: ControllerBase
{
[HttpPost]
public async Task<IActionResult> Receive(...)
{
// ... signature verify, parse event, etc ...
var device = await devices.FindByExternalIdAsync(enodeId, ct);
if (device is null) return Ok(); // skip; not provisioned yet
Properties.Report(device.DeviceId, newReported);
return Ok();
}
}
The lookup uses the unique partial index on
(document->>'ExternalId') WHERE NOT NULL AND deleted_at IS NULL, so
it's O(1).
Listing by it
GET /api/devices?externalId=X returns at most one device. Useful for
operator tooling that wants to find "the BT device for upstream id X".
What about migration?
A device that moves to a different upstream (rare; e.g. user moved their
charger from Enode to Smappee) is modeled as unprovision + re-provision.
There is no in-place mutation API — the new externalId belongs to a
new providerClient, even if it ends up bound to the same asset.
See also
- Aggregator Providers — the end-to-end pattern