@camstack/system 1.2.242 → 1.2.244
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.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +437 -11
- package/dist/builtins/device-manager/device-manager.addon.mjs +437 -11
- package/dist/builtins/device-manager/device-meta-actions.d.ts +97 -0
- package/dist/builtins/device-manager/device-meta-types.d.ts +66 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-DkGEz_3L.mjs → dist-4MS1GNc8.mjs} +308 -3
- package/dist/{dist-BETx_5mH.js → dist-Cz6h6Nc3.js} +325 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-BnTBoLTv.js → retired-settings-keys-DBrIdOQp.js} +1 -1
- package/dist/{retired-settings-keys-BSsaf7n4.mjs → retired-settings-keys-D_5MkKII.mjs} +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-
|
|
1
|
+
import { C as STREAM_PROFILE_META, Cn as EventCategory, Et as parseStreamParamsFormPatch, J as deviceStatusCapability, K as deviceManagerCapability, Pt as runtimeStatePolicyFor, Q as enumerateSchemaFields, R as buildStreamParamsConfigSchema, Sn as sleep$1, Xt as errMsg, Z as enumerateItemArrayFields, Zt as BaseAddon, a as CAMERA_SWITCH_ORDER, f as DeviceStatusSchema, gt as locationIconKey, h as LocationIconIdSchema, hn as isDeviceConfigCap, jt as resolveContainerPrimaryChild, nn as DeviceRole, o as CAP_NAMES_WITH_STATUS, on as WELL_KNOWN_TAB_MAP, q as deviceStateCapability, rn as DeviceType, t as ALL_CAPABILITY_DEFINITIONS, tn as DeviceFeature, wt as normalizeUnit, zt as siteZoneKey } from "../../dist-4MS1GNc8.mjs";
|
|
2
|
+
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-D_5MkKII.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,311 @@ 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
|
+
* Every location that carries a description, sorted by key.
|
|
5036
|
+
*
|
|
5037
|
+
* Only what was WRITTEN — a location with no description is absent, and the
|
|
5038
|
+
* digest then tells the model the location name alone, exactly as it did
|
|
5039
|
+
* before descriptions existed.
|
|
5040
|
+
*/
|
|
5041
|
+
async function listLocationDescriptions(pctx) {
|
|
5042
|
+
const descriptions = (await pctx.metaStore.readStore()).locationDescriptions ?? {};
|
|
5043
|
+
return Object.entries(descriptions).map(([locationKey, description]) => ({
|
|
5044
|
+
locationKey,
|
|
5045
|
+
description
|
|
5046
|
+
})).toSorted((a, b) => a.locationKey.localeCompare(b.locationKey));
|
|
5047
|
+
}
|
|
5048
|
+
/**
|
|
5049
|
+
* Set (or clear) a location's description.
|
|
5050
|
+
*
|
|
5051
|
+
* A BLANK description clears, exactly as `null` does: an empty line handed to
|
|
5052
|
+
* a vision model costs attention and says nothing, so the two must not be
|
|
5053
|
+
* different states. The label is folded with `locationIconKey` — the same key
|
|
5054
|
+
* the icon uses, which is the whole point of this living here.
|
|
5055
|
+
*
|
|
5056
|
+
* Same read-modify-write and same lock as `setLocationIcon`; `writeAddonStore`
|
|
5057
|
+
* re-inserts the whole key range, so an unordered write carries a stale copy
|
|
5058
|
+
* of every other key.
|
|
5059
|
+
*/
|
|
5060
|
+
async function setLocationDescription(pctx, input) {
|
|
5061
|
+
const trimmed = input.location.trim();
|
|
5062
|
+
if (trimmed.length === 0) throw new Error("[device-manager] setLocationDescription: location must be non-empty");
|
|
5063
|
+
const key = locationIconKey(trimmed);
|
|
5064
|
+
const text = input.description === null ? "" : input.description.trim();
|
|
5065
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5066
|
+
const descriptions = (await pctx.metaStore.readStore()).locationDescriptions ?? {};
|
|
5067
|
+
if (text.length === 0) {
|
|
5068
|
+
if (!(key in descriptions)) return;
|
|
5069
|
+
const { [key]: _cleared, ...kept } = descriptions;
|
|
5070
|
+
await pctx.settings.writeAddonStore({ locationDescriptions: kept });
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
if (descriptions[key] === text) return;
|
|
5074
|
+
await pctx.settings.writeAddonStore({ locationDescriptions: {
|
|
5075
|
+
...descriptions,
|
|
5076
|
+
[key]: text
|
|
5077
|
+
} });
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
5080
|
+
/**
|
|
5081
|
+
* Every site zone, with its rooms, sorted by key.
|
|
5082
|
+
*
|
|
5083
|
+
* Membership is inverted HERE rather than stored both ways: the store keeps
|
|
5084
|
+
* `location → zone`, which is what makes "one zone per room" structural, and a
|
|
5085
|
+
* second copy keyed the other way is a second thing that can disagree with the
|
|
5086
|
+
* first.
|
|
5087
|
+
*/
|
|
5088
|
+
async function listSiteZones(pctx) {
|
|
5089
|
+
const store = await pctx.metaStore.readStore();
|
|
5090
|
+
const zones = store.siteZones ?? {};
|
|
5091
|
+
const membership = store.locationZones ?? {};
|
|
5092
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
5093
|
+
for (const [locationKey, zoneKey] of Object.entries(membership)) {
|
|
5094
|
+
const list = rooms.get(zoneKey);
|
|
5095
|
+
if (list === void 0) rooms.set(zoneKey, [locationKey]);
|
|
5096
|
+
else list.push(locationKey);
|
|
5097
|
+
}
|
|
5098
|
+
return Object.entries(zones).map(([zoneKey, record]) => ({
|
|
5099
|
+
zoneKey,
|
|
5100
|
+
label: record.label,
|
|
5101
|
+
icon: record.icon ?? null,
|
|
5102
|
+
locationKeys: (rooms.get(zoneKey) ?? []).toSorted((a, b) => a.localeCompare(b))
|
|
5103
|
+
})).toSorted((a, b) => a.zoneKey.localeCompare(b.zoneKey));
|
|
5104
|
+
}
|
|
5105
|
+
/**
|
|
5106
|
+
* Create a zone, or set the icon of one that already exists.
|
|
5107
|
+
*
|
|
5108
|
+
* `icon: null` clears the icon and KEEPS the zone. Conflating the two would
|
|
5109
|
+
* make a mis-click on "no icon" delete every room's membership — the icon is a
|
|
5110
|
+
* decoration and the grouping is the data.
|
|
5111
|
+
*
|
|
5112
|
+
* `icon` OMITTED leaves whatever icon is there: creating a zone from a screen
|
|
5113
|
+
* with no picker must not silently clear the glyph a previous screen set.
|
|
5114
|
+
*/
|
|
5115
|
+
async function setSiteZone(pctx, input) {
|
|
5116
|
+
const label = input.name.trim();
|
|
5117
|
+
if (label.length === 0) throw new Error("[device-manager] setSiteZone: name must be non-empty");
|
|
5118
|
+
const key = siteZoneKey(label);
|
|
5119
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5120
|
+
const zones = (await pctx.metaStore.readStore()).siteZones ?? {};
|
|
5121
|
+
const existing = zones[key];
|
|
5122
|
+
const icon = input.icon === void 0 ? existing?.icon : input.icon === null ? void 0 : input.icon;
|
|
5123
|
+
const next = icon === void 0 ? { label } : {
|
|
5124
|
+
label,
|
|
5125
|
+
icon
|
|
5126
|
+
};
|
|
5127
|
+
if (existing?.label === next.label && existing.icon === next.icon) return;
|
|
5128
|
+
await pctx.settings.writeAddonStore({ siteZones: {
|
|
5129
|
+
...zones,
|
|
5130
|
+
[key]: next
|
|
5131
|
+
} });
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
/**
|
|
5135
|
+
* Delete a zone. Its LOCATIONS survive, unattached.
|
|
5136
|
+
*
|
|
5137
|
+
* Deleting a grouping must never delete what was grouped — the rooms exist
|
|
5138
|
+
* independently and the devices in them were never touched to begin with.
|
|
5139
|
+
*/
|
|
5140
|
+
async function removeSiteZone(pctx, input) {
|
|
5141
|
+
const key = siteZoneKey(input.name);
|
|
5142
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5143
|
+
const store = await pctx.metaStore.readStore();
|
|
5144
|
+
const zones = store.siteZones ?? {};
|
|
5145
|
+
const membership = store.locationZones ?? {};
|
|
5146
|
+
const hadZone = key in zones;
|
|
5147
|
+
const orphaned = Object.entries(membership).filter(([, zoneKey]) => zoneKey === key);
|
|
5148
|
+
if (!hadZone && orphaned.length === 0) return;
|
|
5149
|
+
const { [key]: _dropped, ...keptZones } = zones;
|
|
5150
|
+
const keptMembership = Object.fromEntries(Object.entries(membership).filter(([, zoneKey]) => zoneKey !== key));
|
|
5151
|
+
await pctx.settings.writeAddonStore({
|
|
5152
|
+
...hadZone ? { siteZones: keptZones } : {},
|
|
5153
|
+
...orphaned.length > 0 ? { locationZones: keptMembership } : {}
|
|
5154
|
+
});
|
|
5155
|
+
});
|
|
5156
|
+
}
|
|
5157
|
+
/**
|
|
5158
|
+
* Rename a zone, carrying its icon AND every room in it.
|
|
5159
|
+
*
|
|
5160
|
+
* It exists so that renaming is not delete-then-recreate, which drops both.
|
|
5161
|
+
* A rename onto an existing zone MERGES: the target keeps its own icon and the
|
|
5162
|
+
* rooms are unioned, the same rule `renameLocation` follows.
|
|
5163
|
+
*/
|
|
5164
|
+
async function renameSiteZone(pctx, input) {
|
|
5165
|
+
const toLabel = input.to.trim();
|
|
5166
|
+
if (toLabel.length === 0) throw new Error("[device-manager] renameSiteZone: target name must be non-empty");
|
|
5167
|
+
const fromKey = siteZoneKey(input.from);
|
|
5168
|
+
const toKey = siteZoneKey(toLabel);
|
|
5169
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5170
|
+
const store = await pctx.metaStore.readStore();
|
|
5171
|
+
const zones = store.siteZones ?? {};
|
|
5172
|
+
const moved = zones[fromKey];
|
|
5173
|
+
if (moved === void 0) return;
|
|
5174
|
+
const { [fromKey]: _dropped, ...rest } = zones;
|
|
5175
|
+
const target = rest[toKey];
|
|
5176
|
+
const nextZones = {
|
|
5177
|
+
...rest,
|
|
5178
|
+
[toKey]: target ?? (moved.icon === void 0 ? { label: toLabel } : {
|
|
5179
|
+
label: toLabel,
|
|
5180
|
+
icon: moved.icon
|
|
5181
|
+
})
|
|
5182
|
+
};
|
|
5183
|
+
const membership = store.locationZones ?? {};
|
|
5184
|
+
const nextMembership = Object.fromEntries(Object.entries(membership).map(([locationKey, zoneKey]) => [locationKey, zoneKey === fromKey ? toKey : zoneKey]));
|
|
5185
|
+
await pctx.settings.writeAddonStore({
|
|
5186
|
+
siteZones: nextZones,
|
|
5187
|
+
locationZones: nextMembership
|
|
5188
|
+
});
|
|
5189
|
+
});
|
|
5190
|
+
}
|
|
5191
|
+
/**
|
|
5192
|
+
* Put a location in a zone, or take it out with `zone: null`.
|
|
5193
|
+
*
|
|
5194
|
+
* ONE slot per location, so a second call moves the room rather than listing it
|
|
5195
|
+
* twice. The zone is created if it does not exist — a screen that assigns
|
|
5196
|
+
* before creating is a normal order, and refusing it would be the only place in
|
|
5197
|
+
* this registry that demands registration first.
|
|
5198
|
+
*/
|
|
5199
|
+
async function setLocationZone(pctx, input) {
|
|
5200
|
+
const location = input.location.trim();
|
|
5201
|
+
if (location.length === 0) throw new Error("[device-manager] setLocationZone: location must be non-empty");
|
|
5202
|
+
const locationKey = locationIconKey(location);
|
|
5203
|
+
const label = input.zone === null ? null : input.zone.trim();
|
|
5204
|
+
if (label !== null && label.length === 0) throw new Error("[device-manager] setLocationZone: zone must be non-empty or null");
|
|
5205
|
+
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5206
|
+
const store = await pctx.metaStore.readStore();
|
|
5207
|
+
const membership = store.locationZones ?? {};
|
|
5208
|
+
if (label === null) {
|
|
5209
|
+
if (!(locationKey in membership)) return;
|
|
5210
|
+
const { [locationKey]: _cleared, ...kept } = membership;
|
|
5211
|
+
await pctx.settings.writeAddonStore({ locationZones: kept });
|
|
5212
|
+
return;
|
|
5213
|
+
}
|
|
5214
|
+
const zoneKey = siteZoneKey(label);
|
|
5215
|
+
const zones = store.siteZones ?? {};
|
|
5216
|
+
const needsZone = !(zoneKey in zones);
|
|
5217
|
+
if (membership[locationKey] === zoneKey && !needsZone) return;
|
|
5218
|
+
await pctx.settings.writeAddonStore({
|
|
5219
|
+
...needsZone ? { siteZones: {
|
|
5220
|
+
...zones,
|
|
5221
|
+
[zoneKey]: { label }
|
|
5222
|
+
} } : {},
|
|
5223
|
+
locationZones: {
|
|
5224
|
+
...membership,
|
|
5225
|
+
[locationKey]: zoneKey
|
|
5226
|
+
}
|
|
5227
|
+
});
|
|
5228
|
+
});
|
|
5229
|
+
}
|
|
5230
|
+
/**
|
|
5231
|
+
* Move a location's zone membership from one key to another, for
|
|
5232
|
+
* `renameLocation`. Same shape as the icon and description movers.
|
|
5233
|
+
*/
|
|
5234
|
+
function renameLocationZone(membership, fromKey, toKey) {
|
|
5235
|
+
const moved = membership[fromKey];
|
|
5236
|
+
if (moved === void 0) return null;
|
|
5237
|
+
if (fromKey === toKey) return null;
|
|
5238
|
+
const { [fromKey]: _dropped, ...rest } = membership;
|
|
5239
|
+
return toKey in rest ? rest : {
|
|
5240
|
+
...rest,
|
|
5241
|
+
[toKey]: moved
|
|
5242
|
+
};
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5245
|
+
* Move a location's description from one key to another, for `renameLocation`.
|
|
5246
|
+
*
|
|
5247
|
+
* Identical rule to {@link renameLocationIcon}, deliberately: a merge keeps the
|
|
5248
|
+
* TARGET's description, because renaming "Sala" into "Salone" moves devices
|
|
5249
|
+
* into Salone and does not re-describe it. Split from the icon mover only
|
|
5250
|
+
* because the value types differ; if one ever changes, the other must.
|
|
5251
|
+
*/
|
|
5252
|
+
function renameLocationDescription(descriptions, fromKey, toKey) {
|
|
5253
|
+
const moved = descriptions[fromKey];
|
|
5254
|
+
if (moved === void 0) return null;
|
|
5255
|
+
if (fromKey === toKey) return null;
|
|
5256
|
+
const { [fromKey]: _dropped, ...rest } = descriptions;
|
|
5257
|
+
return toKey in rest ? rest : {
|
|
5258
|
+
...rest,
|
|
5259
|
+
[toKey]: moved
|
|
5260
|
+
};
|
|
5261
|
+
}
|
|
5262
|
+
/**
|
|
5263
|
+
* Move a location's icon from one key to another, for `renameLocation`.
|
|
5264
|
+
*
|
|
5265
|
+
* Returns `null` when NOTHING changes, so the caller can leave the key out of
|
|
5266
|
+
* its `writeAddonStore` patch instead of re-writing an identical map.
|
|
5267
|
+
*
|
|
5268
|
+
* A rename onto a room that already has an icon is a MERGE, and the surviving
|
|
5269
|
+
* room keeps its own: the operator renaming "Sala" into "Salone" is moving
|
|
5270
|
+
* devices into Salone, not restyling it. The source key is dropped either way.
|
|
5271
|
+
* Pure — builds a new record, mutates nothing.
|
|
5272
|
+
*/
|
|
5273
|
+
function renameLocationIcon(icons, fromKey, toKey) {
|
|
5274
|
+
const moved = icons[fromKey];
|
|
5275
|
+
if (moved === void 0) return null;
|
|
5276
|
+
if (fromKey === toKey) return null;
|
|
5277
|
+
const { [fromKey]: _dropped, ...rest } = icons;
|
|
5278
|
+
return toKey in rest ? rest : {
|
|
5279
|
+
...rest,
|
|
5280
|
+
[toKey]: moved
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
/**
|
|
4978
5284
|
* Remove a label from the curated registry. Match is
|
|
4979
5285
|
* case-insensitive. Devices that still reference this label keep
|
|
4980
5286
|
* their `meta.location` value (the registry is a suggestion
|
|
@@ -5005,13 +5311,23 @@ async function renameLocation(pctx, input) {
|
|
|
5005
5311
|
const fromKey = from.toLowerCase();
|
|
5006
5312
|
const toKey = to.toLowerCase();
|
|
5007
5313
|
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5008
|
-
const
|
|
5009
|
-
|
|
5314
|
+
const store = await pctx.metaStore.readStore();
|
|
5315
|
+
const current = store.locations ?? [];
|
|
5316
|
+
const registered = current.some((l) => l.trim().toLowerCase() === fromKey);
|
|
5317
|
+
const movedIcons = renameLocationIcon(store.locationIcons ?? {}, fromKey, toKey);
|
|
5318
|
+
const movedDescriptions = renameLocationDescription(store.locationDescriptions ?? {}, fromKey, toKey);
|
|
5319
|
+
const movedZones = renameLocationZone(store.locationZones ?? {}, fromKey, toKey);
|
|
5320
|
+
if (!registered && movedIcons === null && movedDescriptions === null && movedZones === null) return;
|
|
5010
5321
|
const kept = current.filter((l) => {
|
|
5011
5322
|
const key = l.trim().toLowerCase();
|
|
5012
5323
|
return key !== fromKey && key !== toKey;
|
|
5013
5324
|
});
|
|
5014
|
-
await pctx.settings.writeAddonStore({
|
|
5325
|
+
await pctx.settings.writeAddonStore({
|
|
5326
|
+
...registered ? { locations: [...kept, to] } : {},
|
|
5327
|
+
...movedIcons !== null ? { locationIcons: movedIcons } : {},
|
|
5328
|
+
...movedDescriptions !== null ? { locationDescriptions: movedDescriptions } : {},
|
|
5329
|
+
...movedZones !== null ? { locationZones: movedZones } : {}
|
|
5330
|
+
});
|
|
5015
5331
|
});
|
|
5016
5332
|
const moved = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
5017
5333
|
const out = [];
|
|
@@ -5049,10 +5365,27 @@ async function removeLocation(pctx, input) {
|
|
|
5049
5365
|
const trimmed = input.name.trim();
|
|
5050
5366
|
if (trimmed.length === 0) return;
|
|
5051
5367
|
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5052
|
-
const
|
|
5053
|
-
const
|
|
5054
|
-
|
|
5055
|
-
|
|
5368
|
+
const store = await pctx.metaStore.readStore();
|
|
5369
|
+
const current = store.locations ?? [];
|
|
5370
|
+
const key = locationIconKey(trimmed);
|
|
5371
|
+
const remaining = current.filter((l) => l.toLowerCase() !== key);
|
|
5372
|
+
const droppedLabel = remaining.length !== current.length;
|
|
5373
|
+
const icons = store.locationIcons ?? {};
|
|
5374
|
+
const droppedIcon = key in icons;
|
|
5375
|
+
const descriptions = store.locationDescriptions ?? {};
|
|
5376
|
+
const droppedDescription = key in descriptions;
|
|
5377
|
+
const membership = store.locationZones ?? {};
|
|
5378
|
+
const droppedZone = key in membership;
|
|
5379
|
+
if (!droppedLabel && !droppedIcon && !droppedDescription && !droppedZone) return;
|
|
5380
|
+
const { [key]: _removedIcon, ...keptIcons } = icons;
|
|
5381
|
+
const { [key]: _removedDescription, ...keptDescriptions } = descriptions;
|
|
5382
|
+
const { [key]: _removedZone, ...keptMembership } = membership;
|
|
5383
|
+
await pctx.settings.writeAddonStore({
|
|
5384
|
+
...droppedLabel ? { locations: remaining } : {},
|
|
5385
|
+
...droppedIcon ? { locationIcons: keptIcons } : {},
|
|
5386
|
+
...droppedDescription ? { locationDescriptions: keptDescriptions } : {},
|
|
5387
|
+
...droppedZone ? { locationZones: keptMembership } : {}
|
|
5388
|
+
});
|
|
5056
5389
|
});
|
|
5057
5390
|
if (input.cascade !== true) return;
|
|
5058
5391
|
const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
@@ -5154,12 +5487,96 @@ function decodeAddonStore(raw) {
|
|
|
5154
5487
|
const nextDeviceId = raw["nextDeviceId"];
|
|
5155
5488
|
const locations = raw["locations"];
|
|
5156
5489
|
const roleDisplayDefaults = raw["roleDisplayDefaults"];
|
|
5490
|
+
const locationIcons = decodeLocationIcons(raw["locationIcons"]);
|
|
5491
|
+
const locationDescriptions = decodeLocationDescriptions(raw["locationDescriptions"]);
|
|
5492
|
+
const siteZones = decodeSiteZones(raw["siteZones"]);
|
|
5493
|
+
const locationZones = decodeStringMap(raw["locationZones"]);
|
|
5157
5494
|
return {
|
|
5158
5495
|
...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
|
|
5159
5496
|
...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
|
|
5160
|
-
...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {}
|
|
5497
|
+
...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {},
|
|
5498
|
+
...locationIcons !== null ? { locationIcons } : {},
|
|
5499
|
+
...locationDescriptions !== null ? { locationDescriptions } : {},
|
|
5500
|
+
...siteZones !== null ? { siteZones } : {},
|
|
5501
|
+
...locationZones !== null ? { locationZones } : {}
|
|
5161
5502
|
};
|
|
5162
5503
|
}
|
|
5504
|
+
/**
|
|
5505
|
+
* Decode the `locationIcons` map, dropping every entry whose value is not a
|
|
5506
|
+
* member of the closed vocabulary.
|
|
5507
|
+
*
|
|
5508
|
+
* Dropping, not passing through: the icon's whole point is that both surfaces
|
|
5509
|
+
* can draw it, and an id from a hand-edit or a newer hub is exactly the value
|
|
5510
|
+
* that draws nothing. Absent reads as "no icon chosen", which every renderer
|
|
5511
|
+
* already degrades to the fallback pin. Returns `null` when the key itself is
|
|
5512
|
+
* absent or of the wrong shape, so the field stays OMITTED rather than being
|
|
5513
|
+
* written back as an empty object.
|
|
5514
|
+
*/
|
|
5515
|
+
function decodeLocationIcons(raw) {
|
|
5516
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
5517
|
+
const out = {};
|
|
5518
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
5519
|
+
const parsed = LocationIconIdSchema.safeParse(value);
|
|
5520
|
+
if (!parsed.success) continue;
|
|
5521
|
+
out[key] = parsed.data;
|
|
5522
|
+
}
|
|
5523
|
+
return out;
|
|
5524
|
+
}
|
|
5525
|
+
/**
|
|
5526
|
+
* Decode the `locationDescriptions` map, dropping every entry that is not a
|
|
5527
|
+
* non-blank string.
|
|
5528
|
+
*
|
|
5529
|
+
* Same posture as the icons: a blank description and a missing one are the
|
|
5530
|
+
* same instruction to every reader, and storing the first would send an empty
|
|
5531
|
+
* line to a vision model. Over-long text is TRUNCATED rather than dropped —
|
|
5532
|
+
* losing the whole description because a hand-edit ran long is a worse answer
|
|
5533
|
+
* than losing its tail.
|
|
5534
|
+
*/
|
|
5535
|
+
function decodeLocationDescriptions(raw) {
|
|
5536
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
5537
|
+
const out = {};
|
|
5538
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
5539
|
+
if (typeof value !== "string") continue;
|
|
5540
|
+
const text = value.trim();
|
|
5541
|
+
if (text.length === 0) continue;
|
|
5542
|
+
out[key] = text.slice(0, 400);
|
|
5543
|
+
}
|
|
5544
|
+
return out;
|
|
5545
|
+
}
|
|
5546
|
+
/**
|
|
5547
|
+
* Decode the zone registry, dropping any entry without a usable label.
|
|
5548
|
+
*
|
|
5549
|
+
* A zone whose label did not survive a hand-edit cannot be shown or renamed —
|
|
5550
|
+
* it would be a row the operator can see the effects of and never name. An
|
|
5551
|
+
* icon outside the vocabulary is dropped on its own, leaving the zone: losing
|
|
5552
|
+
* a glyph is a smaller loss than losing the grouping.
|
|
5553
|
+
*/
|
|
5554
|
+
function decodeSiteZones(raw) {
|
|
5555
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
5556
|
+
const out = {};
|
|
5557
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
5558
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
|
|
5559
|
+
const rec = value;
|
|
5560
|
+
const label = typeof rec["label"] === "string" ? rec["label"].trim() : "";
|
|
5561
|
+
if (label.length === 0) continue;
|
|
5562
|
+
const icon = LocationIconIdSchema.safeParse(rec["icon"]);
|
|
5563
|
+
out[key] = icon.success ? {
|
|
5564
|
+
label,
|
|
5565
|
+
icon: icon.data
|
|
5566
|
+
} : { label };
|
|
5567
|
+
}
|
|
5568
|
+
return out;
|
|
5569
|
+
}
|
|
5570
|
+
/** Decode a plain `string → string` map, dropping anything else. */
|
|
5571
|
+
function decodeStringMap(raw) {
|
|
5572
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
5573
|
+
const out = {};
|
|
5574
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
5575
|
+
if (typeof value !== "string" || value.trim().length === 0) continue;
|
|
5576
|
+
out[key] = value;
|
|
5577
|
+
}
|
|
5578
|
+
return out;
|
|
5579
|
+
}
|
|
5163
5580
|
function isRoleDisplayDefaults(value) {
|
|
5164
5581
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5165
5582
|
}
|
|
@@ -6992,6 +7409,15 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
6992
7409
|
addLocation: (input) => addLocation(pctx, input),
|
|
6993
7410
|
removeLocation: (input) => removeLocation(pctx, input),
|
|
6994
7411
|
renameLocation: (input) => renameLocation(pctx, input),
|
|
7412
|
+
listLocationIcons: () => listLocationIcons(pctx),
|
|
7413
|
+
setLocationIcon: (input) => setLocationIcon(pctx, input),
|
|
7414
|
+
listLocationDescriptions: () => listLocationDescriptions(pctx),
|
|
7415
|
+
setLocationDescription: (input) => setLocationDescription(pctx, input),
|
|
7416
|
+
listSiteZones: () => listSiteZones(pctx),
|
|
7417
|
+
setSiteZone: (input) => setSiteZone(pctx, input),
|
|
7418
|
+
removeSiteZone: (input) => removeSiteZone(pctx, input),
|
|
7419
|
+
renameSiteZone: (input) => renameSiteZone(pctx, input),
|
|
7420
|
+
setLocationZone: (input) => setLocationZone(pctx, input),
|
|
6995
7421
|
listPersistedByAddon: (input) => listPersistedByAddon(pctx, input),
|
|
6996
7422
|
listAll: (input) => listAll(pctx, input),
|
|
6997
7423
|
getDevice: (input) => getDevice(pctx, input),
|
|
@@ -212,6 +212,103 @@ 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']>;
|
|
247
|
+
/**
|
|
248
|
+
* Every location that carries a description, sorted by key.
|
|
249
|
+
*
|
|
250
|
+
* Only what was WRITTEN — a location with no description is absent, and the
|
|
251
|
+
* digest then tells the model the location name alone, exactly as it did
|
|
252
|
+
* before descriptions existed.
|
|
253
|
+
*/
|
|
254
|
+
export declare function listLocationDescriptions(pctx: ProviderContext): ReturnType<IDeviceManagerProvider['listLocationDescriptions']>;
|
|
255
|
+
/**
|
|
256
|
+
* Set (or clear) a location's description.
|
|
257
|
+
*
|
|
258
|
+
* A BLANK description clears, exactly as `null` does: an empty line handed to
|
|
259
|
+
* a vision model costs attention and says nothing, so the two must not be
|
|
260
|
+
* different states. The label is folded with `locationIconKey` — the same key
|
|
261
|
+
* the icon uses, which is the whole point of this living here.
|
|
262
|
+
*
|
|
263
|
+
* Same read-modify-write and same lock as `setLocationIcon`; `writeAddonStore`
|
|
264
|
+
* re-inserts the whole key range, so an unordered write carries a stale copy
|
|
265
|
+
* of every other key.
|
|
266
|
+
*/
|
|
267
|
+
export declare function setLocationDescription(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocationDescription']>[0]): ReturnType<IDeviceManagerProvider['setLocationDescription']>;
|
|
268
|
+
/**
|
|
269
|
+
* Every site zone, with its rooms, sorted by key.
|
|
270
|
+
*
|
|
271
|
+
* Membership is inverted HERE rather than stored both ways: the store keeps
|
|
272
|
+
* `location → zone`, which is what makes "one zone per room" structural, and a
|
|
273
|
+
* second copy keyed the other way is a second thing that can disagree with the
|
|
274
|
+
* first.
|
|
275
|
+
*/
|
|
276
|
+
export declare function listSiteZones(pctx: ProviderContext): ReturnType<IDeviceManagerProvider['listSiteZones']>;
|
|
277
|
+
/**
|
|
278
|
+
* Create a zone, or set the icon of one that already exists.
|
|
279
|
+
*
|
|
280
|
+
* `icon: null` clears the icon and KEEPS the zone. Conflating the two would
|
|
281
|
+
* make a mis-click on "no icon" delete every room's membership — the icon is a
|
|
282
|
+
* decoration and the grouping is the data.
|
|
283
|
+
*
|
|
284
|
+
* `icon` OMITTED leaves whatever icon is there: creating a zone from a screen
|
|
285
|
+
* with no picker must not silently clear the glyph a previous screen set.
|
|
286
|
+
*/
|
|
287
|
+
export declare function setSiteZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setSiteZone']>[0]): ReturnType<IDeviceManagerProvider['setSiteZone']>;
|
|
288
|
+
/**
|
|
289
|
+
* Delete a zone. Its LOCATIONS survive, unattached.
|
|
290
|
+
*
|
|
291
|
+
* Deleting a grouping must never delete what was grouped — the rooms exist
|
|
292
|
+
* independently and the devices in them were never touched to begin with.
|
|
293
|
+
*/
|
|
294
|
+
export declare function removeSiteZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['removeSiteZone']>[0]): ReturnType<IDeviceManagerProvider['removeSiteZone']>;
|
|
295
|
+
/**
|
|
296
|
+
* Rename a zone, carrying its icon AND every room in it.
|
|
297
|
+
*
|
|
298
|
+
* It exists so that renaming is not delete-then-recreate, which drops both.
|
|
299
|
+
* A rename onto an existing zone MERGES: the target keeps its own icon and the
|
|
300
|
+
* rooms are unioned, the same rule `renameLocation` follows.
|
|
301
|
+
*/
|
|
302
|
+
export declare function renameSiteZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['renameSiteZone']>[0]): ReturnType<IDeviceManagerProvider['renameSiteZone']>;
|
|
303
|
+
/**
|
|
304
|
+
* Put a location in a zone, or take it out with `zone: null`.
|
|
305
|
+
*
|
|
306
|
+
* ONE slot per location, so a second call moves the room rather than listing it
|
|
307
|
+
* twice. The zone is created if it does not exist — a screen that assigns
|
|
308
|
+
* before creating is a normal order, and refusing it would be the only place in
|
|
309
|
+
* this registry that demands registration first.
|
|
310
|
+
*/
|
|
311
|
+
export declare function setLocationZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocationZone']>[0]): ReturnType<IDeviceManagerProvider['setLocationZone']>;
|
|
215
312
|
/**
|
|
216
313
|
* Remove a label from the curated registry. Match is
|
|
217
314
|
* case-insensitive. Devices that still reference this label keep
|