Skip to content

Asset identity & calibration batch replace

Operator guide for SafeCall 8.0+ (asset UID + device assignments). Design details: Asset Identity. Remaining gaps (including Zabbix identity): Asset Identity Gaps.

Concepts

  • Asset UID — stable human-readable id (e.g. SN-13D-1). Survives hardware swaps.
  • Device MAC — physical radio/ethernet identity currently attached to the asset.
  • Assignment history — which device operated on which asset, and when.

Location codes (required for real UIDs)

  1. Open Admin → Locations.
  2. Edit each location and set a short code (1–12 alphanumeric, unique among siblings).
  3. The UI shows the concatenated path (e.g. 1 + 3 + D13D).

Without codes on the full path from root → home location, minting a non-legacy asset UID will fail.

Batch remint after codes exist

On existing installs that upgraded before location codes were set, assets stay LEGACY-* until reminted:

  1. Assign location codes (above) and ensure each beacon has a mintable type + home location.
  2. Open Admin → Devices → Assetssettings (gear)Remint LEGACY UIDs.
  3. Review the plan counts → Apply. The server remints beacons to TYPE-LOCATIONPATH-SEQ and rewrites history keys (beacon_events, tracking_logs, button_press_audit, sensor_data, interval .pb files).
  4. Refresh Zabbix LLD afterward (UIDs change).

APIs: GET /api/v1/assets/remint/plan, POST /api/v1/assets/remint/apply (admin).

Configuring a new asset

  1. Import Assets (discovery) creates unknown rows with a LEGACY-{MAC} uid and an open device assignment.
  2. Edit the asset: set type (e.g. sensor) and home location.
  3. Save — server mints asset_uid as TYPE-LOCATIONPATH-SEQ (e.g. SN-13D-1). Install/server_id is not part of the UID. On upgrade, migration 084 remints remaining v1 UIDs and rewrites history keys; refresh Zabbix LLD after upgrade because host UIDs change.

Device Pool (admin UI)

  1. Open Admin → Devices → Device Pool.
  2. Default filters: Search, Status, and Type. Calibration filter appears only when Calibration/Expires columns are enabled via Configure Columns (settings menu).
  3. Default columns: MAC, Asset, Asset UID, Type, Status, Actions. Optional columns (Physical MAC, Calibration, Expires) show when not applicable (e.g. non-sensors).
  4. Use settings → Export CSV for the current filtered view; columns are resizable.
  5. Select assigned beacon, gateway, or e-ink rows → Replace selected, or use the per-row replace control.
  6. In the modal: enter new MAC(s), optional calibration date and reason → ReviewConfirm replace.
  7. Partial success shows a per-target results list; Asset UIDs stay the same (beacon, gateway, and e-ink assignments).

From Admin → Assets, multi-select checkboxes and Replace selected (settings menu) use the same modal. On edit, Device MAC is read-only — swap hardware only via Replace.

Calibration batch replace (~50 sensors)

  1. Calibrate spare sensors off-site; note each MAC and calibration date.
  2. In Device Pool, enable Calibration columns (settings → Configure Columns), then filter Calibration → Expired (or Due soon), or call GET /api/v1/devices?calibration=expired.
  3. Select the assets to swap → Replace selected (or Assets list multi-select / per-row replace).
  4. APIs (automation / scripts):
    • POST /api/v1/beacon/:id/replace-device with { "mac": "...", "last_calibrated_at": <unix_ms> }
    • POST /api/v1/assets/replace-device/batch with up to 200 replacements
  5. Sensors require a calibrated device by default (require_calibrated); uncheck in the UI or pass false only with admin override.
  6. Old devices become available (or mark status in_calibration when sending them out).
  7. Historical sensor readings and events stay under the asset UID.

Merging old replacement sensor assets

Use Merge assets into this only to repair replacements that were previously imported as separate sensor assets. For future hardware swaps, continue using Replace device.

  1. Open Admin → Assets and find the logical asset whose name, location, and Asset UID should survive.
  2. Click Merge assets into this in its row actions (or open Edit asset and use the same button).
  3. Select one or more source sensor assets.
  4. Put the sources in chronological order. The last selected source supplies the device that is currently installed on the surviving asset.
  5. Choose Review merge. Check the inferred reading windows and the number of readings kept/dropped for every source.
  6. Choose Confirm merge only after verifying the order.

The merge uses exclusive windows: all target readings are kept; each source contributes only readings newer than the preceding asset's final reading. Older overlapping source readings are deleted. Source assets are deactivated, their current assignments are moved to the survivor, and the final device is left as the survivor's open assignment.

Prefer merging when the involved sensors are quiet. Apply drains in-memory tracking and button-press queues, rekeys any mid-flight rows onto the survivor, refreshes the beacon cache (including retired sources), and deletes leftover source history keys. Overlapping samples inside exclusive windows are dropped by design.

This operation cannot be undone. Back up both db/safecall.db, db/sensor_data.db, and logs/*.pb before repairing a customer lineage.

Open the surviving sensor's Edit dialog to view Device history (MAC, from, to, and reason). Merge-derived replacement times use the sensor reading boundaries; they are inferred rather than manually recorded service times. The original device's From is that assignment's start when it already falls inside the survivor window; otherwise it is the survivor's first reading (typical after identity import, which stamped started_at at migration time).

APIs:

  • POST /api/v1/beacon/:id/merge/plan with { "source_ids": [136, 205] }
  • POST /api/v1/beacon/:id/merge with the same ordered body
  • GET /api/v1/beacon/:id/assignments

Zabbix item history is not rewritten. Until Zabbix host identity is UID-stable, remove or disable stale MAC-suffixed source hosts after a merge.

Backdated replace (history split)

Use Replace device on a single sensor when two assets should both stay live and only the readings after a cut time should move. This is the opposite shape of Merge: Merge retires sources into one survivor and drops overlapping samples; a backdated replace moves post-cut samples onto the recipient and leaves the donor's earlier samples in place.

  1. Open the recipient asset (the one that should receive the later device).
  2. Choose Replace device.
  3. Enter the new MAC (usually the device currently assigned to the other asset).
  4. Set Replacement time (defaults to this asset's last reading if left empty).
  5. Review the preview: how many donor readings stay vs move, and whether the other asset will be left without a current device.
  6. Check Retire the other asset only for leftover MAC-named rows. Do not retire a real second site.
  7. Confirm. Then, if needed, replace the donor with its next device at the same cut time (order matters: move the stolen MAC off the donor before assigning whatever replaced it).

Omit replaced_at to keep today's swap-at-now behaviour (no history rewrite). Batch, gateway, and e-ink replace stay now-only.

APIs:

  • POST /api/v1/beacon/:id/replace-device/plan with { "mac": "...", "replaced_at": <unix_ms> }
  • POST /api/v1/beacon/:id/replace-device with the same fields plus optional retire_donor

Notifications

Subscribe to ntfy topic {server_id}-calibration for expired calibration alerts (checked shortly after boot, then hourly). Delivery follows server_ntfy_calibration_mode and server_ntfy_calibration_interval_seconds (bulk every 24 hours by default). The hourly checker updates stored condition state; the digest or individual interval decides when ntfy publishes. See SafeCall ntfy / Keryx alert topics.

Asset identity (Phase 6 complete)

Public HTTP/WS and history keys use asset_uid:

  • GET /api/v1/assets/:asset_uid / /api/v1/assets/:asset_uid/info
  • Events, tracking logs, interval .pb files, sensor history
  • WS track-${asset_uid} / track-debug-${asset_uid}

Phase 6a: runtime cache keyed by asset_uid (MAC secondary index); button_press_audit.asset_uid (migration 080). Phase 6g: button_press_audit.mac dropped (migration 082); CSV export is Asset UID only.

Phase 6b–6c: dual-resolve then hard-cut MAC-as-asset path/WS refs. Device/replace/MQTT/BLE stay MAC. Gateway and e-ink admin identity use asset_uid with replace-device (7c/7d); e-ink poll wire stays ?mac=.

Phase 6d: residual MAC→uid rewrite (migration 081 + startup .pb rename/merge); history JOINs and interval reads are uid-only.

Post-upgrade audit (should be clean):

bash
bun scripts/audit/leftover_mac_history.ts --db db/safecall.db --logs logs --fail-on-leftover

The audit treats LEGACY-* and TYPE-PATH-SEQ .pb basenames as asset UIDs (not leftover MAC filenames).

Customer upgrade dry-run (safe.ssh / TST000 pattern)

Before shipping 8.0 to a customer install, dry-run the upgrade on a copy of their DBs while leaving the live 7.7.x process running:

  1. Snapshot the latest daily backup + interval logs (logs are not in core.tar.gz):
bash
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
ROOT=tmp/upgrade-dry-run-TST000/$STAMP
mkdir -p "$ROOT/work/logs"
scp safe.ssh:/opt/safecall/backups/<LATEST>/core.tar.gz "$ROOT/core.tar.gz"
tar -xzf "$ROOT/core.tar.gz" -C "$ROOT/work"
scp 'safe.ssh:/opt/safecall/logs/*.pb' "$ROOT/work/logs/"
cp "$ROOT/work/db/safecall.db" "$ROOT/work/db/safecall.db.pre-upgrade"
cp "$ROOT/work/db/sensor_data.db" "$ROOT/work/db/sensor_data.db.pre-upgrade"
  1. Baseline + offline upgrade (migrations 075088, sensor rewrite, .pb rename, assertions):
bash
bun scripts/upgrade/customer_db_verify.ts --work "$ROOT/work" --baseline
bun scripts/upgrade/customer_db_verify.ts --work "$ROOT/work" --upgrade

Artifacts land next to work/: inventory-baseline.json, inventory-post.json, VALIDATION_REPORT.md.

  1. MQTT soak against the remote Mosquitto on the same host (exact server_mqtt_url=mqtt://localhost:1883 via tunnel). Disable ntfy in-memory so the live install is not double-notified:
bash
ssh -N -L 1883:localhost:1883 safe.ssh   # leave running
# Optional: reset one admin password on the *local copy* and write soak-credentials.json
bun scripts/upgrade/customer_db_soak.ts --work "$ROOT/work" --seconds 120 --port 18088

Expect new sensor_data rows under LEGACY-* (or real UIDs once location codes exist). HTTP smoke covers /api/v1/assets/:asset_uid, /api/v1/devices, gateway get, and the unchanged e-ink poll /eink/display?mac=.

TST000 note: that install has no locations.code values yet, so every beacon/gateway/e-ink keeps a LEGACY-{mac} uid until operators assign codes and mint. Remint map (084) stays empty in that case — still a valid upgrade.

Do not overwrite /opt/safecall on the test host until this dry-run report is clean.

Upgrade notes

  • App version bumped to 8.0.0.
  • Main DB migrations 075088 (location codes, devices/assignments, history rewrite, translations, button_press_audit asset_uid / drop mac, residual pbid rewrite, location-code help, v2 UID remint, gateway asset identity, e-ink asset identity).
  • Sensor DB rewrites sensor_data.dmacasset_uid once on startup.
  • Interval .pb files rename/merge to asset_uid on server start.
  • Zabbix LLD includes {#ASSET_UID} in addition to {#MAC} (current device).
  • After upgrading, confirm event lists and tracking stats still show assets; run the leftover-MAC audit.