Skip to main content

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:

  1. Set it once at provision time, and
  2. 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:

RuleValue
Length1–256 characters
CharsetASCII printable, no whitespace
UniquenessPer provider — different providers MAY use the same id
MutabilityImmutable — 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