Appearance
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
| Entity | Purpose | Stable 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
unknownasset is first typed and a homelocation_idis 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_idis not part of the internal UID. For multi-install Zabbix, prefixserver_idoutside SafeCall if hosts must be unique across sites. LLD emits bareasset_uid. - Existing v1 UIDs (
TST-000-S13D-001style) are reminted to v2 on upgrade (chunk 7b, migration084+ boot sensor/.pbrewrites). Remint map retained inasset_uid_remint_map.
Type codes:
| Code | Beacon / entity type |
|---|---|
SN | sensor (includes fridge / virtual-MAC assets) |
PN | panic |
WT | watch |
NV | navigation |
GW | gateway (gateways asset; chunk 7c) |
EK | e-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_calibratedis derived:last_calibrated_atis set andnow < 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
| Phase | Behavior |
|---|---|
| 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 replacementsGET /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_uidbeacons.physical_mac→ device (fridge NIC)beacon_events.pbid,tracking_logs.pbid→ asset_uiddevice_beacons.mac,device_gateways.mac→ device MAC (discovery)gateways.mac— current device MAC (denormalized); durable id →asset_uid(GW-…, migration085)beacon_gateway_sightings.beacon_mac/gateway_mac→ keep device MAC (RF); gateway_mac cascade-updated on replace-devicebutton_press_audit.asset_uid→ asset_uid only (Phase 6g droppedmac; migration082)eink_displays.mac— current device MAC (denormalized, colon format); durable id →asset_uid(EK-…, migration087)sensor_data.dmac→ asset_uid (Phase 4 rewrite); readers dual-key mac+uid viasensor_history_keys(Phase 6f: alerts / e-ink / maintenance; history+grid already dual)
Ingest / runtime
- MQTT
dmac/gmac→ device 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_normalizestays colon-formatted. cache.tsprimary key → asset_uid (MAC secondary index for MQTT); not-seen timers survive replace-deviceevents.ts/tracking.tshistory keys → asset_uid (MAC fallback)lib/src/sensor_mac.tsvirtual 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)
- Upgrade binary (runs migrations 075+).
- Assign
locations.codeon the location tree (Phase 1). - For legacy assets: set type + location, then Assets → settings → Remint LEGACY UIDs (batch + history rewrite), or mint one-by-one on edit.
- Use Replace device for calibration batches; do not edit MAC as asset identity.
- Subscribe ntfy
{server_id}-calibrationfor expiry alerts.