Skip to content

Asset Identity & Device Lifecycle

Design freeze for the Asset / Device / Assignment model. See also the project roadmap plan asset_identity_roadmap.

Remaining gaps (Zabbix host identity and LEGACY ops): Asset Identity Gaps. Ordered sensor history merge is shipped.

Entity model

EntityPurposeStable key
Asset (beacon) (beacons table)Logical spot/role (fridge position, watch storage, nav slot). Holds type, name, thresholds, location, active.asset_uid; numeric id for admin CRUD
Asset (gateway) (gateways table)Logical gateway placement (tag, room, map coords, location).asset_uid (GW-…); numeric id for admin CRUD
Asset (e-ink) (eink_displays table)Logical display placement (name, location, layout, approval state).asset_uid (EK-…); numeric id for admin CRUD
Device (devices table)Physical hardware. MAC is unique here. Calibration lives on the device.mac (wire / virtual MAC as stored today)
Assignment (device_assignments)Which device operated on which asset, and when. Exactly one of asset_id (beacon), gateway_id (gateway), or eink_id (e-ink).at most one open row per asset and per device

Ingest rule (beacons): MQTT always emits hardware dmac → resolve open assignment → asset → store/alert under asset_uid.

Ingest rule (gateways): MQTT gmac stays device MAC on the wire; resolve to the gateway asset via current gateways.mac / open gateway_id assignment. Firmware does not emit asset_uid.

Ingest rule (e-ink): HTTP poll GET /eink/display?mac= stays device MAC on the wire; resolve via eink_displays.mac / open eink_id assignment. Auto-register creates a pending asset with LEGACY-{mac} until a coded home location is set. Designer layout cells store sensor_uid (sensor asset UID); they do not follow a swapped radio.

Fridge coexistence: Virtual MAC (FE…) and physical_mac remain a device concern. Asset UID never uses FE… as identity. Wire dmac still maps through sensor_virtual_mac to the device MAC used in assignments.

Asset UID grammar (v2)

ASSET_UID := TYPE "-" LOCATION_PATH "-" SEQ
TYPE           := 2 A–Z chars
LOCATION_PATH  := concatenation of location.code from root → home location (e.g. 13D)
SEQ            := 1–3 decimal digits, no forced zero-pad (1 … 999), unique per (TYPE, LOCATION_PATH)

Example: SN-13D-1

  • Generated when an unknown asset is first typed and a home location_id is set.
  • Refused if any ancestor location (including self) lacks a non-empty code.
  • Mobile assets (watch) still require a home/storage location for the path.
  • Legacy backfill may use LEGACY-{normalized_mac} until an operator assigns type+location.
  • Install / server_id is not part of the internal UID. For multi-install Zabbix, prefix server_id outside SafeCall if hosts must be unique across sites. LLD emits bare asset_uid.
  • Existing v1 UIDs (TST-000-S13D-001 style) are reminted to v2 on upgrade (chunk 7b, migration 084 + boot sensor/.pb rewrites). Remint map retained in asset_uid_remint_map.

Type codes:

CodeBeacon / entity type
SNsensor (includes fridge / virtual-MAC assets)
PNpanic
WTwatch
NVnavigation
GWgateway (gateways asset; chunk 7c)
EKe-ink display (eink_displays asset; chunk 7d)
unknown → no UID yet

Gateway mint: GW-LOCATION_PATH-SEQ when location_id has a full coded path; otherwise LEGACY-{mac}. Primary radio MAC is denormalized on gateways.mac and owned by devices; mac2 remains gateway metadata only (not a second assignment). Admin MAC changes go through Replace device (cascade-updates beacon_gateway_sightings.gateway_mac / panic_event_gateways.gateway_mac so FKs to gateways(mac) stay valid).

E-ink mint: EK-LOCATION_PATH-SEQ when location_id has a full coded path; otherwise LEGACY-{mac} (including auto-registered pending displays). Display MAC is denormalized on eink_displays.mac (colon format for poll) and owned by devices. Admin hardware swap via Replace device. Poll contract stays ?mac=.

Calibration policy

  • Default interval: 730 days (2 years) on devices.calibration_interval_days.
  • is_calibrated is derived: last_calibrated_at is set and now < last_calibrated_at + interval.
  • Enforcement priority: sensors first (warn/block on assign); schema applies to all device types.
  • Expiry notification: ntfy topic {server_id}-calibration (hourly check; shared ntfy cooldown). Inventory: ntfy topics.

Public API shape

PhaseBehavior
Additive (2–3)Existing /api/v1/beacon/:id, /api/v1/beacon/:mac/info keep working; responses gain asset_uid and device fields
Cutover (4)Prefer /api/v1/assets/:asset_uid/...; dual-read MAC paths for one major
Cutover (6b)Dual-resolve refs (asset_uid OR MAC); first-party clients prefer uid; MAC deprecated
Cleanup (6c)Uid-only resolve; drop MAC WS aliases (MAC remains on device/replace APIs)
History (6d)Residual MAC→uid rewrite + drop history dual-JOINs / .pb MAC fallback

Recommended new routes (Phase 3+):

  • POST /api/v1/beacon/:id/replace-device — body { mac, physical_mac?, reason, last_calibrated_at?, replaced_at?, retire_donor? }
  • POST /api/v1/beacon/:id/replace-device/plan — backdated sensor split preview { mac, replaced_at? }
  • POST /api/v1/assets/replace-device/batch — array of replacements
  • GET /api/v1/devices — pool (assigned / unassigned / calibration status)
  • GET /api/v1/assets/:asset_uid/... — uid-only (Phase 6c)

Version strategy

Major version bump when Phase 4 cutover ships (breaking: history keys and LLD prefer asset_uid; Zabbix {#ASSET_UID} primary, {#MAC} = current device). Client installs upgrade via normal install/migrate path; main DB migrations + sensor DB rewrite run on startup.

Non-goals

  • Multi-tenant multiple server_ids in one DB
  • Map/floor scale “calibration”
  • Firmware emitting asset UIDs (wire stays MAC)

Deferred (not permanent non-goals): None remaining for Asset/Device entity expansion. Gateway fold is done (7c); e-ink fold is done (7d). V1 → v2 UID remint is done (7b). Firmware still does not emit asset UIDs (wire stays MAC).

MAC touchpoint inventory

Legend: → asset_uid | → device MAC | → dual (cutover)

DB

  • beacons.mac — current device MAC (denormalized) after split; durable id → asset_uid
  • beacons.physical_macdevice (fridge NIC)
  • beacon_events.pbid, tracking_logs.pbidasset_uid
  • device_beacons.mac, device_gateways.macdevice MAC (discovery)
  • gateways.mac — current device MAC (denormalized); durable id → asset_uid (GW-…, migration 085)
  • beacon_gateway_sightings.beacon_mac / gateway_mac → keep device MAC (RF); gateway_mac cascade-updated on replace-device
  • button_press_audit.asset_uidasset_uid only (Phase 6g dropped mac; migration 082)
  • eink_displays.mac — current device MAC (denormalized, colon format); durable id → asset_uid (EK-…, migration 087)
  • sensor_data.dmacasset_uid (Phase 4 rewrite); readers dual-key mac+uid via sensor_history_keys (Phase 6f: alerts / e-ink / maintenance; history+grid already dual)

Ingest / runtime

  • MQTT dmac/gmacdevice MAC; resolve → asset via assignment / cache MAC index
  • Wire MAC compare/normalize → lib/src/mac_normalize.ts (strip non-hex → UPPER); used by cache, devices, integration, device_assignment, beacon-config, gateway-id tools, maintenance, gateway_mac_normalize (Phase 6h). eink_mac_normalize stays colon-formatted.
  • cache.ts primary key → asset_uid (MAC secondary index for MQTT); not-seen timers survive replace-device
  • events.ts / tracking.ts history keys → asset_uid (MAC fallback)
  • lib/src/sensor_mac.ts virtual MAC → device identity only
  • KB Pro / beacon-config BLE target → device MAC

HTTP (asset identity → dual → asset_uid)

Phase 6c: path segments historically named :mac / :id must be asset_uid. asset_resolve_ref looks up WHERE asset_uid = ref only. Surfaces: /api/v1/beacon/:mac/info, /api/v1/assets/:asset_uid, /api/v1/tracking/:id, /api/v1/user/rtls/beacons/:mac/zone-intervals, /api/v1/integration/beacons/:mac/location, WS track-* / track-debug-* (uid topics only; payload field still named mac). Device/replace APIs stay MAC; gateway and e-ink admin identity include asset_uid with replace-device (7c/7d).

Phase 6d: migration 081 residual pbid rewrite + startup .pb rename/merge; events/integration/stats/devices JOINs and interval reads are uid-only. Verify with bun scripts/audit/leftover_mac_history.ts --fail-on-leftover.

Stay device MAC

MQTT gmac, beacon-config beacon_mac / gmac for BLE, e-ink display poll ?mac= (wire). Gateway/e-ink admin CRUD uses numeric id + asset_uid; hardware swap via replace-device.

Web / mobile

Assets UI: asset_uid primary, MAC = current device. Mobile BLE still matches uname; bundles may expose asset_uid.

Integration / Zabbix

{#ASSET_UID} primary; {#MAC} current device. Bridge resolves wire dmac → asset host.

Upgrade path (operator)

  1. Upgrade binary (runs migrations 075+).
  2. Assign locations.code on the location tree (Phase 1).
  3. For legacy assets: set type + location, then Assets → settings → Remint LEGACY UIDs (batch + history rewrite), or mint one-by-one on edit.
  4. Use Replace device for calibration batches; do not edit MAC as asset identity.
  5. Subscribe ntfy {server_id}-calibration for expiry alerts.