@camstack/system 1.2.242 → 1.2.243

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +129 -11
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +129 -11
  15. package/dist/builtins/device-manager/device-meta-actions.d.ts +32 -0
  16. package/dist/builtins/device-manager/device-meta-types.d.ts +17 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +1 -1
  36. package/dist/builtins/snapshot/index.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/system-config/system-config.addon.js +1 -1
  44. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  45. package/dist/builtins/winston-logging/index.js +1 -1
  46. package/dist/builtins/winston-logging/index.mjs +1 -1
  47. package/dist/{dist-DkGEz_3L.mjs → dist-B4htkDVv.mjs} +103 -3
  48. package/dist/{dist-BETx_5mH.js → dist-D6kqBW1G.js} +114 -2
  49. package/dist/index.js +1 -1
  50. package/dist/index.mjs +1 -1
  51. package/dist/{retired-settings-keys-BnTBoLTv.js → retired-settings-keys-BPIyHR4_.js} +1 -1
  52. package/dist/{retired-settings-keys-BSsaf7n4.mjs → retired-settings-keys-C9w4b4TV.mjs} +1 -1
  53. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, bn as EventCategory, k as addonPagesCapability, qt as errMsg } from "../../dist-DkGEz_3L.mjs";
1
+ import { A as addonPagesCapability, Sn as EventCategory, Xt as BaseAddon, Yt as errMsg } from "../../dist-B4htkDVv.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, bn as EventCategory, j as addonWidgetsCapability, qt as errMsg } from "../../dist-DkGEz_3L.mjs";
1
+ import { M as addonWidgetsCapability, Sn as EventCategory, Xt as BaseAddon, Yt as errMsg } from "../../dist-B4htkDVv.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, M as alertsCapability, bn as EventCategory, n as AlertSchema, qt as errMsg, sn as createEvent } from "../../dist-DkGEz_3L.mjs";
1
+ import { N as alertsCapability, Sn as EventCategory, Xt as BaseAddon, Yt as errMsg, ln as createEvent, n as AlertSchema } from "../../dist-B4htkDVv.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { Bt as storageOccupancyCapability, Jt as BaseAddon, P as backupCapability, bn as EventCategory, yt as mayWriteToLocation } from "../../dist-DkGEz_3L.mjs";
1
+ import { F as backupCapability, Ht as storageOccupancyCapability, Sn as EventCategory, Xt as BaseAddon, xt as mayWriteToLocation } from "../../dist-B4htkDVv.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, ht as logDestinationCapability } from "../../dist-DkGEz_3L.mjs";
1
+ import { Xt as BaseAddon, _t as logDestinationCapability } from "../../dist-B4htkDVv.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-BETx_5mH.js");
3
+ const require_dist = require("../../dist-D6kqBW1G.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { B as coreBlockIdFromAddonId, Jt as BaseAddon, V as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, qt as errMsg, s as CORE_BLOCKS_ADDON_ID, z as coreBlockAddonId } from "../../dist-DkGEz_3L.mjs";
1
+ import { B as coreBlockAddonId, H as coreBlocksCapability, V as coreBlockIdFromAddonId, Xt as BaseAddon, Yt as errMsg, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-B4htkDVv.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-BnTBoLTv.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-BPIyHR4_.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -3928,7 +3928,8 @@ async function runLocationBackfillAtBoot(pctx, bootRows) {
3928
3928
  * setLocation, setType, setIntegrationId, setLinkDeviceId,
3929
3929
  * setPrimaryChildEntityId, setChildLayout, setRole, setDisplay,
3930
3930
  * applyInitialMeta, setMetadata, setDisabled), and the location registry
3931
- * (listLocations, addLocation, removeLocation).
3931
+ * (listLocations, addLocation, removeLocation, renameLocation) with its icon
3932
+ * map (listLocationIcons, setLocationIcon).
3932
3933
  *
3933
3934
  * EXACT ordering and side effects preserved — notably registerDevice's
3934
3935
  * name-precedence reconcile + field-preservation spreads, the
@@ -4980,6 +4981,83 @@ async function addLocation(pctx, input) {
4980
4981
  });
4981
4982
  }
4982
4983
  /**
4984
+ * Every location that carries an icon, as `(locationKey, icon)` pairs.
4985
+ *
4986
+ * ADDITIVE beside `listLocations`, which still answers `string[]` — the
4987
+ * autocomplete, the HA adoption import and the recorder all read a list of
4988
+ * LABELS and a shape change there would break every one of them. A surface
4989
+ * that wants icons asks for them and joins on `locationIconKey`.
4990
+ *
4991
+ * Returns only what was actually stored. A location with no assignment is
4992
+ * ABSENT, never `map-pin`: the fallback belongs to the renderer
4993
+ * (`resolveLocationIcon`), and folding it in here would make "the operator
4994
+ * picked the pin" indistinguishable from "the operator picked nothing" —
4995
+ * which is the difference an icon picker has to draw.
4996
+ */
4997
+ async function listLocationIcons(pctx) {
4998
+ const icons = (await pctx.metaStore.readStore()).locationIcons ?? {};
4999
+ return Object.entries(icons).map(([locationKey, icon]) => ({
5000
+ locationKey,
5001
+ icon
5002
+ })).toSorted((a, b) => a.locationKey.localeCompare(b.locationKey));
5003
+ }
5004
+ /**
5005
+ * Set (or clear) the icon of a location label.
5006
+ *
5007
+ * Keyed on `locationIconKey` — trimmed + lower-cased, the equality the
5008
+ * registry matches on everywhere — so a case-only rename keeps the icon and
5009
+ * two spellings of one room can never carry two different icons.
5010
+ *
5011
+ * The label need NOT be registered: `setLocation` accepts a free-form string
5012
+ * and `listLocations` folds in whatever devices carry, so requiring
5013
+ * registration here would leave exactly the ad-hoc rooms unable to have an
5014
+ * icon. An empty label throws — there is no room to name.
5015
+ *
5016
+ * Same read-modify-write and same lock as `addLocation`: the map is one key of
5017
+ * the addon store, and `writeAddonStore` re-inserts the whole key range, so an
5018
+ * unordered write carries a stale copy of every other key.
5019
+ */
5020
+ async function setLocationIcon(pctx, input) {
5021
+ const trimmed = input.location.trim();
5022
+ if (trimmed.length === 0) throw new Error("[device-manager] setLocationIcon: location must be non-empty");
5023
+ const key = require_dist.locationIconKey(trimmed);
5024
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5025
+ const icons = (await pctx.metaStore.readStore()).locationIcons ?? {};
5026
+ if (input.icon === null) {
5027
+ if (!(key in icons)) return;
5028
+ const { [key]: _cleared, ...kept } = icons;
5029
+ await pctx.settings.writeAddonStore({ locationIcons: kept });
5030
+ return;
5031
+ }
5032
+ if (icons[key] === input.icon) return;
5033
+ await pctx.settings.writeAddonStore({ locationIcons: {
5034
+ ...icons,
5035
+ [key]: input.icon
5036
+ } });
5037
+ });
5038
+ }
5039
+ /**
5040
+ * Move a location's icon from one key to another, for `renameLocation`.
5041
+ *
5042
+ * Returns `null` when NOTHING changes, so the caller can leave the key out of
5043
+ * its `writeAddonStore` patch instead of re-writing an identical map.
5044
+ *
5045
+ * A rename onto a room that already has an icon is a MERGE, and the surviving
5046
+ * room keeps its own: the operator renaming "Sala" into "Salone" is moving
5047
+ * devices into Salone, not restyling it. The source key is dropped either way.
5048
+ * Pure — builds a new record, mutates nothing.
5049
+ */
5050
+ function renameLocationIcon(icons, fromKey, toKey) {
5051
+ const moved = icons[fromKey];
5052
+ if (moved === void 0) return null;
5053
+ if (fromKey === toKey) return null;
5054
+ const { [fromKey]: _dropped, ...rest } = icons;
5055
+ return toKey in rest ? rest : {
5056
+ ...rest,
5057
+ [toKey]: moved
5058
+ };
5059
+ }
5060
+ /**
4983
5061
  * Remove a label from the curated registry. Match is
4984
5062
  * case-insensitive. Devices that still reference this label keep
4985
5063
  * their `meta.location` value (the registry is a suggestion
@@ -5010,13 +5088,19 @@ async function renameLocation(pctx, input) {
5010
5088
  const fromKey = from.toLowerCase();
5011
5089
  const toKey = to.toLowerCase();
5012
5090
  await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5013
- const current = (await pctx.metaStore.readStore()).locations ?? [];
5014
- if (!current.some((l) => l.trim().toLowerCase() === fromKey)) return;
5091
+ const store = await pctx.metaStore.readStore();
5092
+ const current = store.locations ?? [];
5093
+ const registered = current.some((l) => l.trim().toLowerCase() === fromKey);
5094
+ const movedIcons = renameLocationIcon(store.locationIcons ?? {}, fromKey, toKey);
5095
+ if (!registered && movedIcons === null) return;
5015
5096
  const kept = current.filter((l) => {
5016
5097
  const key = l.trim().toLowerCase();
5017
5098
  return key !== fromKey && key !== toKey;
5018
5099
  });
5019
- await pctx.settings.writeAddonStore({ locations: [...kept, to] });
5100
+ await pctx.settings.writeAddonStore({
5101
+ ...registered ? { locations: [...kept, to] } : {},
5102
+ ...movedIcons !== null ? { locationIcons: movedIcons } : {}
5103
+ });
5020
5104
  });
5021
5105
  const moved = await pctx.metaStore.withMetaWriteLock(async () => {
5022
5106
  const out = [];
@@ -5054,10 +5138,19 @@ async function removeLocation(pctx, input) {
5054
5138
  const trimmed = input.name.trim();
5055
5139
  if (trimmed.length === 0) return;
5056
5140
  await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5057
- const current = (await pctx.metaStore.readStore()).locations ?? [];
5058
- const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
5059
- if (remaining.length === current.length) return;
5060
- await pctx.settings.writeAddonStore({ locations: remaining });
5141
+ const store = await pctx.metaStore.readStore();
5142
+ const current = store.locations ?? [];
5143
+ const key = require_dist.locationIconKey(trimmed);
5144
+ const remaining = current.filter((l) => l.toLowerCase() !== key);
5145
+ const droppedLabel = remaining.length !== current.length;
5146
+ const icons = store.locationIcons ?? {};
5147
+ const droppedIcon = key in icons;
5148
+ if (!droppedLabel && !droppedIcon) return;
5149
+ const { [key]: _removedIcon, ...keptIcons } = icons;
5150
+ await pctx.settings.writeAddonStore({
5151
+ ...droppedLabel ? { locations: remaining } : {},
5152
+ ...droppedIcon ? { locationIcons: keptIcons } : {}
5153
+ });
5061
5154
  });
5062
5155
  if (input.cascade !== true) return;
5063
5156
  const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
@@ -5159,12 +5252,35 @@ function decodeAddonStore(raw) {
5159
5252
  const nextDeviceId = raw["nextDeviceId"];
5160
5253
  const locations = raw["locations"];
5161
5254
  const roleDisplayDefaults = raw["roleDisplayDefaults"];
5255
+ const locationIcons = decodeLocationIcons(raw["locationIcons"]);
5162
5256
  return {
5163
5257
  ...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
5164
5258
  ...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
5165
- ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {}
5259
+ ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {},
5260
+ ...locationIcons !== null ? { locationIcons } : {}
5166
5261
  };
5167
5262
  }
5263
+ /**
5264
+ * Decode the `locationIcons` map, dropping every entry whose value is not a
5265
+ * member of the closed vocabulary.
5266
+ *
5267
+ * Dropping, not passing through: the icon's whole point is that both surfaces
5268
+ * can draw it, and an id from a hand-edit or a newer hub is exactly the value
5269
+ * that draws nothing. Absent reads as "no icon chosen", which every renderer
5270
+ * already degrades to the fallback pin. Returns `null` when the key itself is
5271
+ * absent or of the wrong shape, so the field stays OMITTED rather than being
5272
+ * written back as an empty object.
5273
+ */
5274
+ function decodeLocationIcons(raw) {
5275
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5276
+ const out = {};
5277
+ for (const [key, value] of Object.entries(raw)) {
5278
+ const parsed = require_dist.LocationIconIdSchema.safeParse(value);
5279
+ if (!parsed.success) continue;
5280
+ out[key] = parsed.data;
5281
+ }
5282
+ return out;
5283
+ }
5168
5284
  function isRoleDisplayDefaults(value) {
5169
5285
  return value !== null && typeof value === "object" && !Array.isArray(value);
5170
5286
  }
@@ -6997,6 +7113,8 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
6997
7113
  addLocation: (input) => addLocation(pctx, input),
6998
7114
  removeLocation: (input) => removeLocation(pctx, input),
6999
7115
  renameLocation: (input) => renameLocation(pctx, input),
7116
+ listLocationIcons: () => listLocationIcons(pctx),
7117
+ setLocationIcon: (input) => setLocationIcon(pctx, input),
7000
7118
  listPersistedByAddon: (input) => listPersistedByAddon(pctx, input),
7001
7119
  listAll: (input) => listAll(pctx, input),
7002
7120
  getDevice: (input) => getDevice(pctx, input),
@@ -1,5 +1,5 @@
1
- import { $t as DeviceRole, G as deviceManagerCapability, Jt as BaseAddon, K as deviceStateCapability, L as buildStreamParamsConfigSchema, Mt as runtimeStatePolicyFor, Qt as DeviceFeature, S as STREAM_PROFILE_META, St as normalizeUnit, X as enumerateItemArrayFields, Z as enumerateSchemaFields, a as CAMERA_SWITCH_ORDER, bn as EventCategory, en as DeviceType, f as DeviceStatusSchema, fn as isDeviceConfigCap, kt as resolveContainerPrimaryChild, o as CAP_NAMES_WITH_STATUS, q as deviceStatusCapability, qt as errMsg, rn as WELL_KNOWN_TAB_MAP, t as ALL_CAPABILITY_DEFINITIONS, wt as parseStreamParamsFormPatch, yn as sleep$1 } from "../../dist-DkGEz_3L.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-BSsaf7n4.mjs";
1
+ import { C as STREAM_PROFILE_META, Et as parseStreamParamsFormPatch, J as deviceStatusCapability, K as deviceManagerCapability, Pt as runtimeStatePolicyFor, Q as enumerateSchemaFields, R as buildStreamParamsConfigSchema, Sn as EventCategory, Xt as BaseAddon, Yt as errMsg, Z as enumerateItemArrayFields, a as CAMERA_SWITCH_ORDER, an as WELL_KNOWN_TAB_MAP, en as DeviceFeature, f as DeviceStatusSchema, gt as locationIconKey, h as LocationIconIdSchema, jt as resolveContainerPrimaryChild, mn as isDeviceConfigCap, nn as DeviceType, o as CAP_NAMES_WITH_STATUS, q as deviceStateCapability, t as ALL_CAPABILITY_DEFINITIONS, tn as DeviceRole, wt as normalizeUnit, xn as sleep$1 } from "../../dist-B4htkDVv.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-C9w4b4TV.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -3923,7 +3923,8 @@ async function runLocationBackfillAtBoot(pctx, bootRows) {
3923
3923
  * setLocation, setType, setIntegrationId, setLinkDeviceId,
3924
3924
  * setPrimaryChildEntityId, setChildLayout, setRole, setDisplay,
3925
3925
  * applyInitialMeta, setMetadata, setDisabled), and the location registry
3926
- * (listLocations, addLocation, removeLocation).
3926
+ * (listLocations, addLocation, removeLocation, renameLocation) with its icon
3927
+ * map (listLocationIcons, setLocationIcon).
3927
3928
  *
3928
3929
  * EXACT ordering and side effects preserved — notably registerDevice's
3929
3930
  * name-precedence reconcile + field-preservation spreads, the
@@ -4975,6 +4976,83 @@ async function addLocation(pctx, input) {
4975
4976
  });
4976
4977
  }
4977
4978
  /**
4979
+ * Every location that carries an icon, as `(locationKey, icon)` pairs.
4980
+ *
4981
+ * ADDITIVE beside `listLocations`, which still answers `string[]` — the
4982
+ * autocomplete, the HA adoption import and the recorder all read a list of
4983
+ * LABELS and a shape change there would break every one of them. A surface
4984
+ * that wants icons asks for them and joins on `locationIconKey`.
4985
+ *
4986
+ * Returns only what was actually stored. A location with no assignment is
4987
+ * ABSENT, never `map-pin`: the fallback belongs to the renderer
4988
+ * (`resolveLocationIcon`), and folding it in here would make "the operator
4989
+ * picked the pin" indistinguishable from "the operator picked nothing" —
4990
+ * which is the difference an icon picker has to draw.
4991
+ */
4992
+ async function listLocationIcons(pctx) {
4993
+ const icons = (await pctx.metaStore.readStore()).locationIcons ?? {};
4994
+ return Object.entries(icons).map(([locationKey, icon]) => ({
4995
+ locationKey,
4996
+ icon
4997
+ })).toSorted((a, b) => a.locationKey.localeCompare(b.locationKey));
4998
+ }
4999
+ /**
5000
+ * Set (or clear) the icon of a location label.
5001
+ *
5002
+ * Keyed on `locationIconKey` — trimmed + lower-cased, the equality the
5003
+ * registry matches on everywhere — so a case-only rename keeps the icon and
5004
+ * two spellings of one room can never carry two different icons.
5005
+ *
5006
+ * The label need NOT be registered: `setLocation` accepts a free-form string
5007
+ * and `listLocations` folds in whatever devices carry, so requiring
5008
+ * registration here would leave exactly the ad-hoc rooms unable to have an
5009
+ * icon. An empty label throws — there is no room to name.
5010
+ *
5011
+ * Same read-modify-write and same lock as `addLocation`: the map is one key of
5012
+ * the addon store, and `writeAddonStore` re-inserts the whole key range, so an
5013
+ * unordered write carries a stale copy of every other key.
5014
+ */
5015
+ async function setLocationIcon(pctx, input) {
5016
+ const trimmed = input.location.trim();
5017
+ if (trimmed.length === 0) throw new Error("[device-manager] setLocationIcon: location must be non-empty");
5018
+ const key = locationIconKey(trimmed);
5019
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5020
+ const icons = (await pctx.metaStore.readStore()).locationIcons ?? {};
5021
+ if (input.icon === null) {
5022
+ if (!(key in icons)) return;
5023
+ const { [key]: _cleared, ...kept } = icons;
5024
+ await pctx.settings.writeAddonStore({ locationIcons: kept });
5025
+ return;
5026
+ }
5027
+ if (icons[key] === input.icon) return;
5028
+ await pctx.settings.writeAddonStore({ locationIcons: {
5029
+ ...icons,
5030
+ [key]: input.icon
5031
+ } });
5032
+ });
5033
+ }
5034
+ /**
5035
+ * Move a location's icon from one key to another, for `renameLocation`.
5036
+ *
5037
+ * Returns `null` when NOTHING changes, so the caller can leave the key out of
5038
+ * its `writeAddonStore` patch instead of re-writing an identical map.
5039
+ *
5040
+ * A rename onto a room that already has an icon is a MERGE, and the surviving
5041
+ * room keeps its own: the operator renaming "Sala" into "Salone" is moving
5042
+ * devices into Salone, not restyling it. The source key is dropped either way.
5043
+ * Pure — builds a new record, mutates nothing.
5044
+ */
5045
+ function renameLocationIcon(icons, fromKey, toKey) {
5046
+ const moved = icons[fromKey];
5047
+ if (moved === void 0) return null;
5048
+ if (fromKey === toKey) return null;
5049
+ const { [fromKey]: _dropped, ...rest } = icons;
5050
+ return toKey in rest ? rest : {
5051
+ ...rest,
5052
+ [toKey]: moved
5053
+ };
5054
+ }
5055
+ /**
4978
5056
  * Remove a label from the curated registry. Match is
4979
5057
  * case-insensitive. Devices that still reference this label keep
4980
5058
  * their `meta.location` value (the registry is a suggestion
@@ -5005,13 +5083,19 @@ async function renameLocation(pctx, input) {
5005
5083
  const fromKey = from.toLowerCase();
5006
5084
  const toKey = to.toLowerCase();
5007
5085
  await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5008
- const current = (await pctx.metaStore.readStore()).locations ?? [];
5009
- if (!current.some((l) => l.trim().toLowerCase() === fromKey)) return;
5086
+ const store = await pctx.metaStore.readStore();
5087
+ const current = store.locations ?? [];
5088
+ const registered = current.some((l) => l.trim().toLowerCase() === fromKey);
5089
+ const movedIcons = renameLocationIcon(store.locationIcons ?? {}, fromKey, toKey);
5090
+ if (!registered && movedIcons === null) return;
5010
5091
  const kept = current.filter((l) => {
5011
5092
  const key = l.trim().toLowerCase();
5012
5093
  return key !== fromKey && key !== toKey;
5013
5094
  });
5014
- await pctx.settings.writeAddonStore({ locations: [...kept, to] });
5095
+ await pctx.settings.writeAddonStore({
5096
+ ...registered ? { locations: [...kept, to] } : {},
5097
+ ...movedIcons !== null ? { locationIcons: movedIcons } : {}
5098
+ });
5015
5099
  });
5016
5100
  const moved = await pctx.metaStore.withMetaWriteLock(async () => {
5017
5101
  const out = [];
@@ -5049,10 +5133,19 @@ async function removeLocation(pctx, input) {
5049
5133
  const trimmed = input.name.trim();
5050
5134
  if (trimmed.length === 0) return;
5051
5135
  await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5052
- const current = (await pctx.metaStore.readStore()).locations ?? [];
5053
- const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
5054
- if (remaining.length === current.length) return;
5055
- await pctx.settings.writeAddonStore({ locations: remaining });
5136
+ const store = await pctx.metaStore.readStore();
5137
+ const current = store.locations ?? [];
5138
+ const key = locationIconKey(trimmed);
5139
+ const remaining = current.filter((l) => l.toLowerCase() !== key);
5140
+ const droppedLabel = remaining.length !== current.length;
5141
+ const icons = store.locationIcons ?? {};
5142
+ const droppedIcon = key in icons;
5143
+ if (!droppedLabel && !droppedIcon) return;
5144
+ const { [key]: _removedIcon, ...keptIcons } = icons;
5145
+ await pctx.settings.writeAddonStore({
5146
+ ...droppedLabel ? { locations: remaining } : {},
5147
+ ...droppedIcon ? { locationIcons: keptIcons } : {}
5148
+ });
5056
5149
  });
5057
5150
  if (input.cascade !== true) return;
5058
5151
  const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
@@ -5154,12 +5247,35 @@ function decodeAddonStore(raw) {
5154
5247
  const nextDeviceId = raw["nextDeviceId"];
5155
5248
  const locations = raw["locations"];
5156
5249
  const roleDisplayDefaults = raw["roleDisplayDefaults"];
5250
+ const locationIcons = decodeLocationIcons(raw["locationIcons"]);
5157
5251
  return {
5158
5252
  ...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
5159
5253
  ...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
5160
- ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {}
5254
+ ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {},
5255
+ ...locationIcons !== null ? { locationIcons } : {}
5161
5256
  };
5162
5257
  }
5258
+ /**
5259
+ * Decode the `locationIcons` map, dropping every entry whose value is not a
5260
+ * member of the closed vocabulary.
5261
+ *
5262
+ * Dropping, not passing through: the icon's whole point is that both surfaces
5263
+ * can draw it, and an id from a hand-edit or a newer hub is exactly the value
5264
+ * that draws nothing. Absent reads as "no icon chosen", which every renderer
5265
+ * already degrades to the fallback pin. Returns `null` when the key itself is
5266
+ * absent or of the wrong shape, so the field stays OMITTED rather than being
5267
+ * written back as an empty object.
5268
+ */
5269
+ function decodeLocationIcons(raw) {
5270
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5271
+ const out = {};
5272
+ for (const [key, value] of Object.entries(raw)) {
5273
+ const parsed = LocationIconIdSchema.safeParse(value);
5274
+ if (!parsed.success) continue;
5275
+ out[key] = parsed.data;
5276
+ }
5277
+ return out;
5278
+ }
5163
5279
  function isRoleDisplayDefaults(value) {
5164
5280
  return value !== null && typeof value === "object" && !Array.isArray(value);
5165
5281
  }
@@ -6992,6 +7108,8 @@ var DeviceManagerAddon = class extends BaseAddon {
6992
7108
  addLocation: (input) => addLocation(pctx, input),
6993
7109
  removeLocation: (input) => removeLocation(pctx, input),
6994
7110
  renameLocation: (input) => renameLocation(pctx, input),
7111
+ listLocationIcons: () => listLocationIcons(pctx),
7112
+ setLocationIcon: (input) => setLocationIcon(pctx, input),
6995
7113
  listPersistedByAddon: (input) => listPersistedByAddon(pctx, input),
6996
7114
  listAll: (input) => listAll(pctx, input),
6997
7115
  getDevice: (input) => getDevice(pctx, input),
@@ -212,6 +212,38 @@ export declare function listLocations(pctx: ProviderContext): ReturnType<IDevice
212
212
  * meaningful label.
213
213
  */
214
214
  export declare function addLocation(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['addLocation']>[0]): ReturnType<IDeviceManagerProvider['addLocation']>;
215
+ /**
216
+ * Every location that carries an icon, as `(locationKey, icon)` pairs.
217
+ *
218
+ * ADDITIVE beside `listLocations`, which still answers `string[]` — the
219
+ * autocomplete, the HA adoption import and the recorder all read a list of
220
+ * LABELS and a shape change there would break every one of them. A surface
221
+ * that wants icons asks for them and joins on `locationIconKey`.
222
+ *
223
+ * Returns only what was actually stored. A location with no assignment is
224
+ * ABSENT, never `map-pin`: the fallback belongs to the renderer
225
+ * (`resolveLocationIcon`), and folding it in here would make "the operator
226
+ * picked the pin" indistinguishable from "the operator picked nothing" —
227
+ * which is the difference an icon picker has to draw.
228
+ */
229
+ export declare function listLocationIcons(pctx: ProviderContext): ReturnType<IDeviceManagerProvider['listLocationIcons']>;
230
+ /**
231
+ * Set (or clear) the icon of a location label.
232
+ *
233
+ * Keyed on `locationIconKey` — trimmed + lower-cased, the equality the
234
+ * registry matches on everywhere — so a case-only rename keeps the icon and
235
+ * two spellings of one room can never carry two different icons.
236
+ *
237
+ * The label need NOT be registered: `setLocation` accepts a free-form string
238
+ * and `listLocations` folds in whatever devices carry, so requiring
239
+ * registration here would leave exactly the ad-hoc rooms unable to have an
240
+ * icon. An empty label throws — there is no room to name.
241
+ *
242
+ * Same read-modify-write and same lock as `addLocation`: the map is one key of
243
+ * the addon store, and `writeAddonStore` re-inserts the whole key range, so an
244
+ * unordered write carries a stale copy of every other key.
245
+ */
246
+ export declare function setLocationIcon(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocationIcon']>[0]): ReturnType<IDeviceManagerProvider['setLocationIcon']>;
215
247
  /**
216
248
  * Remove a label from the curated registry. Match is
217
249
  * case-insensitive. Devices that still reference this label keep
@@ -1,4 +1,4 @@
1
- import { ChildLayout, DeviceDisplayOverride, RoleDisplayDefault } from '@camstack/types';
1
+ import { ChildLayout, DeviceDisplayOverride, LocationIconId, RoleDisplayDefault } from '@camstack/types';
2
2
  /**
3
3
  * Wire shape matching `z.infer<typeof SettingsSchemaWithValuesSchema>` —
4
4
  * duplicated as a plain interface because importing the Zod schema across
@@ -161,6 +161,22 @@ export interface AddonStore {
161
161
  * the operator forgot to register a label before assigning it.
162
162
  */
163
163
  locations?: readonly string[];
164
+ /**
165
+ * Icon per location, keyed by `locationIconKey(label)` — trimmed and
166
+ * lower-cased, the same equality the registry itself matches on. Lives HERE,
167
+ * beside `locations`, because that is where a location exists at all: a
168
+ * location is a label, not a row, so there is no record to hang an icon on.
169
+ *
170
+ * Keyed on the folded label rather than the operator's casing on purpose. A
171
+ * case-only rename (`cucina` → `Cucina`) is a real edit this repo supports,
172
+ * and a map keyed on the displayed spelling would silently orphan its entry
173
+ * on exactly that edit — the icon would vanish and nothing would say why.
174
+ *
175
+ * A value outside `LOCATION_ICON_IDS` is DROPPED on decode, not trusted: the
176
+ * store is JSON on disk and a renderer handed an unknown id can only draw a
177
+ * hole. Dropping it degrades the location to the fallback pin instead.
178
+ */
179
+ locationIcons?: Readonly<Record<string, LocationIconId>>;
164
180
  /** Operator-authored per-role display defaults (unit/precision/icon), keyed
165
181
  * by `DeviceRole` string. Resolution merges these UNDER any per-device
166
182
  * `display` override. Written whole-record by `setRoleDisplayDefaults`
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { F as bareAddonId, J as doorbellCapability, Jt as BaseAddon, Q as evaluateSensorEdge, b as SOURCE_DEVICE_TYPES, bn as EventCategory, ct as isSameAddonId, en as DeviceType, lt as isSourceCap, qt as errMsg, sn as createEvent, y as SOURCE_CAPS } from "../../dist-DkGEz_3L.mjs";
1
+ import { $ as evaluateSensorEdge, I as bareAddonId, Sn as EventCategory, Xt as BaseAddon, Y as doorbellCapability, Yt as errMsg, b as SOURCE_CAPS, ln as createEvent, lt as isSameAddonId, nn as DeviceType, ut as isSourceCap, x as SOURCE_DEVICE_TYPES } from "../../dist-B4htkDVv.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, ht as logDestinationCapability } from "../../dist-DkGEz_3L.mjs";
1
+ import { Xt as BaseAddon, _t as logDestinationCapability } from "../../dist-B4htkDVv.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-BETx_5mH.js");
3
+ const require_dist = require("../../dist-D6kqBW1G.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, bn as EventCategory, qt as errMsg, sn as createEvent, v as RecordingStatusSchema } from "../../dist-DkGEz_3L.mjs";
1
+ import { Sn as EventCategory, Xt as BaseAddon, Yt as errMsg, ln as createEvent, y as RecordingStatusSchema } from "../../dist-B4htkDVv.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
6
+ const require_dist = require("../../dist-D6kqBW1G.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { C as ScopedTokenSchema, Jt as BaseAddon, N as authProviderCapability, O as UserRecordSchema, Ut as userManagementCapability, r as ApiKeyRecordSchema } from "../../dist-DkGEz_3L.mjs";
2
+ import { Gt as userManagementCapability, P as authProviderCapability, Xt as BaseAddon, k as UserRecordSchema, r as ApiKeyRecordSchema, w as ScopedTokenSchema } from "../../dist-B4htkDVv.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";