@camstack/system 1.1.23 → 1.1.24
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/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/device-manager/device-link-overlay.d.ts +15 -5
- package/dist/builtins/device-manager/device-link-resolver.d.ts +11 -0
- package/dist/builtins/device-manager/device-manager.addon.js +255 -33
- package/dist/builtins/device-manager/device-manager.addon.mjs +256 -34
- package/dist/builtins/device-manager/device-meta-actions.d.ts +21 -0
- package/dist/builtins/device-manager/device-meta-types.d.ts +12 -1
- package/dist/builtins/device-manager/device-projection.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{manifest-python-deps-hDWMDe_b.mjs → manifest-python-deps-CPJXzrZt.mjs} +12 -0
- package/dist/{manifest-python-deps-D7iR07uA.js → manifest-python-deps-XWJwKYDx.js} +12 -0
- package/package.json +1 -1
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
2
|
+
const require_manifest_python_deps = require("./manifest-python-deps-XWJwKYDx.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
|
|
4
4
|
let node_fs = require("node:fs");
|
|
5
5
|
node_fs = require_chunk.__toESM(node_fs);
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as resolveAddonClass, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, yt as installManifestNativeDeps } from "./manifest-python-deps-
|
|
1
|
+
import { I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as resolveAddonClass, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, yt as installManifestNativeDeps } from "./manifest-python-deps-CPJXzrZt.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -2,7 +2,12 @@ import { DeviceLink } from '@camstack/types';
|
|
|
2
2
|
/** A target link with its source already resolved to a live numeric id. */
|
|
3
3
|
export interface ResolvedTargetLink {
|
|
4
4
|
readonly link: DeviceLink;
|
|
5
|
+
/** Resolved source device id for scalar sources; `-1` sentinel for a literal
|
|
6
|
+
* source AND for an expression source (whose per-binding ids live below). */
|
|
5
7
|
readonly sourceDeviceId: number;
|
|
8
|
+
/** Expression links only: binding name → resolved source device id. Literal
|
|
9
|
+
* bindings and unresolved bindings are ABSENT from the map. */
|
|
10
|
+
readonly bindingSourceIds?: Readonly<Record<string, number>>;
|
|
6
11
|
}
|
|
7
12
|
/** A target that depends on a given source slice (for reactive re-emit). */
|
|
8
13
|
export interface DependentLink {
|
|
@@ -15,9 +20,14 @@ export interface LinkIndexes {
|
|
|
15
20
|
/** `${sourceDeviceId}:${sourceCap}` → targets to recompute on a source change. */
|
|
16
21
|
readonly dependents: Map<string, DependentLink[]>;
|
|
17
22
|
}
|
|
23
|
+
/** One resolved link entry fed to `buildLinkIndexes`. */
|
|
24
|
+
export interface LinkIndexEntry {
|
|
25
|
+
readonly targetDeviceId: number;
|
|
26
|
+
readonly link: DeviceLink;
|
|
27
|
+
/** Resolved scalar source id; `-1` for literal/expression sources. */
|
|
28
|
+
readonly sourceDeviceId: number;
|
|
29
|
+
/** Expression links: binding name → resolved source device id. */
|
|
30
|
+
readonly bindingSourceIds?: Readonly<Record<string, number>>;
|
|
31
|
+
}
|
|
18
32
|
/** Build both lookup maps from resolved link entries. Pure — order-preserving. */
|
|
19
|
-
export declare function buildLinkIndexes(entries: ReadonlyArray<
|
|
20
|
-
targetDeviceId: number;
|
|
21
|
-
link: DeviceLink;
|
|
22
|
-
sourceDeviceId: number;
|
|
23
|
-
}>): LinkIndexes;
|
|
33
|
+
export declare function buildLinkIndexes(entries: ReadonlyArray<LinkIndexEntry>): LinkIndexes;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CapabilityStatusItemArray, DeviceLink } from '@camstack/types';
|
|
3
|
+
import { ResolvedTargetLink } from './device-link-overlay.js';
|
|
3
4
|
/** One resolved source reading paired with the link that produced it. The
|
|
4
5
|
* `sourceValue` is already transformed; `undefined` skips the overlay. */
|
|
5
6
|
export interface ResolvedLinkValue {
|
|
@@ -16,6 +17,16 @@ export interface ResolvedLinkValue {
|
|
|
16
17
|
* `source.kind` directly.
|
|
17
18
|
*/
|
|
18
19
|
export declare function resolveLinkValue(link: DeviceLink, readField: (cap: string, fieldPath: string) => unknown): unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Sync-channel expression resolution: bindings read from the mirror via
|
|
22
|
+
* `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
|
|
23
|
+
* on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
|
|
24
|
+
* binding id (dangling source) or a non-primitive read resolves to `null`.
|
|
25
|
+
* Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
|
|
26
|
+
* the mirror path is hot and churn-free; the async channel logs. Compile
|
|
27
|
+
* failures cost one parse ever thanks to the negative LRU compile cache. Pure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveExpressionLinkValue(rl: ResolvedTargetLink, readField: (deviceId: number, cap: string, fieldPath: string) => unknown, now: number): unknown;
|
|
19
30
|
/**
|
|
20
31
|
* Overlay transformed source values onto `base` by dot-path, then validate the
|
|
21
32
|
* result against the target cap's `statusSchema`. On validation failure the
|
|
@@ -300,7 +300,8 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
|
|
|
300
300
|
...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
|
|
301
301
|
...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
|
|
302
302
|
...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
|
|
303
|
-
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {}
|
|
303
|
+
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
|
|
304
|
+
...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
|
|
304
305
|
};
|
|
305
306
|
}
|
|
306
307
|
function resolveDeviceById(registry, deviceId) {
|
|
@@ -1371,20 +1372,41 @@ var DeviceEventPropagator = class {
|
|
|
1371
1372
|
function nodeKey(deviceId, cap) {
|
|
1372
1373
|
return `${deviceId}:${cap}`;
|
|
1373
1374
|
}
|
|
1375
|
+
/** The target's container stableId — its parent's (falling back to its own for
|
|
1376
|
+
* a top-level target). Sibling FIELD sources resolve relative to this, the same
|
|
1377
|
+
* rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
|
|
1378
|
+
function containerStableId(targetRow, rowById) {
|
|
1379
|
+
return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
|
|
1380
|
+
}
|
|
1381
|
+
/** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
|
|
1382
|
+
* null when it is a literal or resolves to no known device. */
|
|
1383
|
+
function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
|
|
1384
|
+
if (binding.kind === "literal") return null;
|
|
1385
|
+
const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
|
|
1386
|
+
const srcId = idByStableId.get(wantedStableId);
|
|
1387
|
+
if (srcId === void 0) return null;
|
|
1388
|
+
return nodeKey(srcId, binding.cap);
|
|
1389
|
+
}
|
|
1374
1390
|
/**
|
|
1375
|
-
* Resolve a link source to
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1391
|
+
* Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
|
|
1392
|
+
* A literal source depends on nothing; a field/global source depends on exactly
|
|
1393
|
+
* one node; an EXPRESSION source depends on one node per resolvable non-literal
|
|
1394
|
+
* binding. Unresolvable (dangling) bindings contribute no edge — they cannot
|
|
1395
|
+
* loop while dangling; a cycle that only becomes resolvable later is bounded by
|
|
1396
|
+
* the resolve-time re-entrancy guard.
|
|
1380
1397
|
*/
|
|
1381
|
-
function
|
|
1398
|
+
function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
|
|
1382
1399
|
const src = link.source;
|
|
1383
|
-
if (src.kind === "
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1400
|
+
if (src.kind === "expression") {
|
|
1401
|
+
const nodes = [];
|
|
1402
|
+
for (const binding of Object.values(src.bindings)) {
|
|
1403
|
+
const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
|
|
1404
|
+
if (node !== null) nodes.push(node);
|
|
1405
|
+
}
|
|
1406
|
+
return nodes;
|
|
1407
|
+
}
|
|
1408
|
+
const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
|
|
1409
|
+
return node !== null ? [node] : [];
|
|
1388
1410
|
}
|
|
1389
1411
|
/**
|
|
1390
1412
|
* Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
|
|
@@ -1402,11 +1424,11 @@ function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
|
|
|
1402
1424
|
const dependsOn = /* @__PURE__ */ new Map();
|
|
1403
1425
|
const addEdges = (row, links) => {
|
|
1404
1426
|
for (const link of links) {
|
|
1405
|
-
const
|
|
1406
|
-
if (
|
|
1427
|
+
const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
|
|
1428
|
+
if (srcNodes.length === 0) continue;
|
|
1407
1429
|
const tNode = nodeKey(row.id, link.target.cap);
|
|
1408
1430
|
const list = dependsOn.get(tNode) ?? [];
|
|
1409
|
-
list.push(srcNode);
|
|
1431
|
+
for (const srcNode of srcNodes) list.push(srcNode);
|
|
1410
1432
|
dependsOn.set(tNode, list);
|
|
1411
1433
|
}
|
|
1412
1434
|
};
|
|
@@ -1473,7 +1495,7 @@ function dfsFindCycle(root, dependsOn) {
|
|
|
1473
1495
|
* removeDevice), config persistence (persistConfig, loadConfig), the meta
|
|
1474
1496
|
* surface load (loadMeta, loadRuntimeState), every meta setter (setName,
|
|
1475
1497
|
* setLocation, setType, setIntegrationId, setLinkDeviceId,
|
|
1476
|
-
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole,
|
|
1498
|
+
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
|
|
1477
1499
|
* applyInitialMeta, setMetadata, setDisabled), and the location registry
|
|
1478
1500
|
* (listLocations, addLocation, removeLocation).
|
|
1479
1501
|
*
|
|
@@ -1582,6 +1604,7 @@ async function registerDevice(pctx, input) {
|
|
|
1582
1604
|
...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
|
|
1583
1605
|
...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
|
|
1584
1606
|
...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
|
|
1607
|
+
...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
|
|
1585
1608
|
parentDeviceId,
|
|
1586
1609
|
id,
|
|
1587
1610
|
features: featuresArr,
|
|
@@ -2023,6 +2046,11 @@ async function setDeviceLinks(pctx, input) {
|
|
|
2023
2046
|
const { deviceId, deviceLinks } = input;
|
|
2024
2047
|
const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
|
|
2025
2048
|
if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
|
|
2049
|
+
for (const link of deviceLinks) {
|
|
2050
|
+
if (link.source.kind !== "expression") continue;
|
|
2051
|
+
const err = (0, _camstack_types.validateExpressionSource)(link.source);
|
|
2052
|
+
if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
|
|
2053
|
+
}
|
|
2026
2054
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2027
2055
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2028
2056
|
if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
|
|
@@ -2093,6 +2121,84 @@ async function setRole(pctx, input) {
|
|
|
2093
2121
|
});
|
|
2094
2122
|
}
|
|
2095
2123
|
/**
|
|
2124
|
+
* Normalize an operator-authored display override at write time so the render
|
|
2125
|
+
* path's `UNIT_TABLE` lookups always hit canonical spellings — maps `unit` and
|
|
2126
|
+
* every `perCap[*].unit` through `normalizeUnit`. Pure: rebuilds new objects,
|
|
2127
|
+
* never mutates the input. A unit `normalizeUnit` cannot canonicalize is left
|
|
2128
|
+
* as-is (the render path refuses conversion for unknown spellings).
|
|
2129
|
+
*/
|
|
2130
|
+
function normalizeDisplayOverride(display) {
|
|
2131
|
+
return {
|
|
2132
|
+
...display,
|
|
2133
|
+
...display.unit !== void 0 ? { unit: (0, _camstack_types.normalizeUnit)(display.unit) ?? display.unit } : {},
|
|
2134
|
+
...display.perCap !== void 0 ? { perCap: Object.fromEntries(Object.entries(display.perCap).map(([cap, refine]) => [cap, refine.unit !== void 0 ? {
|
|
2135
|
+
...refine,
|
|
2136
|
+
unit: (0, _camstack_types.normalizeUnit)(refine.unit) ?? refine.unit
|
|
2137
|
+
} : refine])) } : {}
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Set (or clear) the per-device display override on a device's meta row.
|
|
2142
|
+
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
2143
|
+
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
2144
|
+
* projection contract, never persists `display: undefined`). Override units are
|
|
2145
|
+
* normalized at write so the render path always looks up canonical spellings.
|
|
2146
|
+
*/
|
|
2147
|
+
async function setDisplay(pctx, input) {
|
|
2148
|
+
const { deviceId, display } = input;
|
|
2149
|
+
const normalized = display === null ? null : normalizeDisplayOverride(display);
|
|
2150
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2151
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2152
|
+
if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
|
|
2153
|
+
const { meta: m } = persisted;
|
|
2154
|
+
const key = String(deviceId);
|
|
2155
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2156
|
+
const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
|
|
2157
|
+
...m,
|
|
2158
|
+
display: normalized
|
|
2159
|
+
};
|
|
2160
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2161
|
+
...allMeta,
|
|
2162
|
+
[key]: nextRow
|
|
2163
|
+
} });
|
|
2164
|
+
});
|
|
2165
|
+
pctx.host.ctx.eventBus.emit({
|
|
2166
|
+
id: (0, node_crypto.randomUUID)(),
|
|
2167
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2168
|
+
source: {
|
|
2169
|
+
type: "device",
|
|
2170
|
+
id: deviceId
|
|
2171
|
+
},
|
|
2172
|
+
category: _camstack_types.EventCategory.DeviceMetaChanged,
|
|
2173
|
+
data: {
|
|
2174
|
+
deviceId,
|
|
2175
|
+
field: "display",
|
|
2176
|
+
value: normalized
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Read the operator-authored per-role display defaults. Empty record when none
|
|
2182
|
+
* set. Not per-device — a plain top-level-key read.
|
|
2183
|
+
*/
|
|
2184
|
+
async function getRoleDisplayDefaults(pctx, _input) {
|
|
2185
|
+
return { defaults: (await pctx.metaStore.readStore()).roleDisplayDefaults ?? {} };
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Replace the per-role display defaults whole-record (full replace). Override
|
|
2189
|
+
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
2190
|
+
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
2191
|
+
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
2192
|
+
* is emitted; the UI invalidates its own query on mutate.
|
|
2193
|
+
*/
|
|
2194
|
+
async function setRoleDisplayDefaults(pctx, input) {
|
|
2195
|
+
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
2196
|
+
...def,
|
|
2197
|
+
unit: (0, _camstack_types.normalizeUnit)(def.unit) ?? def.unit
|
|
2198
|
+
} : def]));
|
|
2199
|
+
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2096
2202
|
* Batched meta pre-seed. Applies every provided field to the
|
|
2097
2203
|
* device's meta row in ONE read-modify-write under a single
|
|
2098
2204
|
* `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
|
|
@@ -2337,23 +2443,33 @@ async function removeLocation(pctx, input) {
|
|
|
2337
2443
|
function buildLinkIndexes(entries) {
|
|
2338
2444
|
const targets = /* @__PURE__ */ new Map();
|
|
2339
2445
|
const dependents = /* @__PURE__ */ new Map();
|
|
2340
|
-
for (const { targetDeviceId, link, sourceDeviceId } of entries) {
|
|
2446
|
+
for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
|
|
2341
2447
|
const tKey = `${targetDeviceId}:${link.target.cap}`;
|
|
2342
2448
|
const tList = targets.get(tKey) ?? [];
|
|
2343
2449
|
tList.push({
|
|
2344
2450
|
link,
|
|
2345
|
-
sourceDeviceId
|
|
2451
|
+
sourceDeviceId,
|
|
2452
|
+
...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
|
|
2346
2453
|
});
|
|
2347
2454
|
targets.set(tKey, tList);
|
|
2348
|
-
|
|
2349
|
-
const sKey = `${
|
|
2455
|
+
const addDependent = (sourceId, sourceCap) => {
|
|
2456
|
+
const sKey = `${sourceId}:${sourceCap}`;
|
|
2350
2457
|
const sList = dependents.get(sKey) ?? [];
|
|
2351
2458
|
sList.push({
|
|
2352
2459
|
targetDeviceId,
|
|
2353
2460
|
targetCap: link.target.cap
|
|
2354
2461
|
});
|
|
2355
2462
|
dependents.set(sKey, sList);
|
|
2463
|
+
};
|
|
2464
|
+
if (link.source.kind === "expression") {
|
|
2465
|
+
if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
|
|
2466
|
+
const binding = link.source.bindings[name];
|
|
2467
|
+
if (binding === void 0 || binding.kind === "literal") continue;
|
|
2468
|
+
addDependent(bindingDeviceId, binding.cap);
|
|
2469
|
+
}
|
|
2470
|
+
continue;
|
|
2356
2471
|
}
|
|
2472
|
+
if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
|
|
2357
2473
|
}
|
|
2358
2474
|
return {
|
|
2359
2475
|
targets,
|
|
@@ -2508,6 +2624,23 @@ var DeviceMetaStore = class {
|
|
|
2508
2624
|
if (container === void 0) continue;
|
|
2509
2625
|
for (const link of targetMeta.deviceLinks ?? []) {
|
|
2510
2626
|
const src = link.source;
|
|
2627
|
+
if (src.kind === "expression") {
|
|
2628
|
+
const bindingSourceIds = {};
|
|
2629
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
2630
|
+
if (b.kind === "literal") continue;
|
|
2631
|
+
const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
|
|
2632
|
+
expectedSources.add(wanted);
|
|
2633
|
+
const bid = idByStableId.get(wanted);
|
|
2634
|
+
if (bid !== void 0) bindingSourceIds[name] = bid;
|
|
2635
|
+
}
|
|
2636
|
+
entries.push({
|
|
2637
|
+
targetDeviceId: targetId,
|
|
2638
|
+
link,
|
|
2639
|
+
sourceDeviceId: -1,
|
|
2640
|
+
bindingSourceIds
|
|
2641
|
+
});
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2511
2644
|
if (src.kind === "literal") {
|
|
2512
2645
|
entries.push({
|
|
2513
2646
|
targetDeviceId: targetId,
|
|
@@ -2625,6 +2758,7 @@ async function listAll(pctx, input) {
|
|
|
2625
2758
|
...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2626
2759
|
...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2627
2760
|
...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2761
|
+
...m?.display !== void 0 ? { display: m.display } : {},
|
|
2628
2762
|
...(() => {
|
|
2629
2763
|
const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
2630
2764
|
return si !== void 0 ? { sourceInfo: si } : {};
|
|
@@ -2673,6 +2807,7 @@ async function getDevice(pctx, input) {
|
|
|
2673
2807
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2674
2808
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2675
2809
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2810
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2676
2811
|
...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
|
|
2677
2812
|
};
|
|
2678
2813
|
}
|
|
@@ -2735,6 +2870,7 @@ async function getChildren(pctx, input) {
|
|
|
2735
2870
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2736
2871
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2737
2872
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2873
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2738
2874
|
...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
|
|
2739
2875
|
});
|
|
2740
2876
|
}
|
|
@@ -3074,7 +3210,37 @@ function isRecord(x) {
|
|
|
3074
3210
|
* `source.kind` directly.
|
|
3075
3211
|
*/
|
|
3076
3212
|
function resolveLinkValue(link, readField) {
|
|
3077
|
-
|
|
3213
|
+
const src = link.source;
|
|
3214
|
+
if (src.kind === "expression") return void 0;
|
|
3215
|
+
return (0, _camstack_types.applyTransform)(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Sync-channel expression resolution: bindings read from the mirror via
|
|
3219
|
+
* `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
|
|
3220
|
+
* on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
|
|
3221
|
+
* binding id (dangling source) or a non-primitive read resolves to `null`.
|
|
3222
|
+
* Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
|
|
3223
|
+
* the mirror path is hot and churn-free; the async channel logs. Compile
|
|
3224
|
+
* failures cost one parse ever thanks to the negative LRU compile cache. Pure.
|
|
3225
|
+
*/
|
|
3226
|
+
function resolveExpressionLinkValue(rl, readField, now) {
|
|
3227
|
+
const src = rl.link.source;
|
|
3228
|
+
if (src.kind !== "expression") return void 0;
|
|
3229
|
+
const bindingValues = {};
|
|
3230
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3231
|
+
if (b.kind === "literal") {
|
|
3232
|
+
bindingValues[name] = b.value;
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
const bId = rl.bindingSourceIds?.[name];
|
|
3236
|
+
if (bId === void 0) {
|
|
3237
|
+
bindingValues[name] = null;
|
|
3238
|
+
continue;
|
|
3239
|
+
}
|
|
3240
|
+
bindingValues[name] = (0, _camstack_types.toExpressionValue)(readField(bId, b.cap, b.fieldPath)) ?? null;
|
|
3241
|
+
}
|
|
3242
|
+
const result = (0, _camstack_types.evaluateLinkExpression)(src.expr, bindingValues, now);
|
|
3243
|
+
return result.ok ? (0, _camstack_types.applyTransform)(result.value, rl.link.transform) : void 0;
|
|
3078
3244
|
}
|
|
3079
3245
|
/** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
|
|
3080
3246
|
* concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
|
|
@@ -3395,9 +3561,10 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
3395
3561
|
const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
|
|
3396
3562
|
const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
|
|
3397
3563
|
if (!links || links.length === 0) return raw ? { ...raw } : null;
|
|
3564
|
+
const now = Date.now();
|
|
3398
3565
|
const resolved = links.map((rl) => ({
|
|
3399
3566
|
link: rl.link,
|
|
3400
|
-
sourceValue: resolveLinkValue(rl.link, (srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3567
|
+
sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3401
3568
|
}));
|
|
3402
3569
|
const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
|
|
3403
3570
|
const schema = capStatus?.schema;
|
|
@@ -3588,6 +3755,39 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3588
3755
|
const parentMeta = allMeta[String(parentId)];
|
|
3589
3756
|
if (parentMeta) containerStableId = parentMeta.stableId;
|
|
3590
3757
|
}
|
|
3758
|
+
const now = Date.now();
|
|
3759
|
+
const statusMemo = /* @__PURE__ */ new Map();
|
|
3760
|
+
const readSourceStatus = async (srcId, srcCap) => {
|
|
3761
|
+
const key = `${srcId}:${srcCap}`;
|
|
3762
|
+
const cached = statusMemo.get(key);
|
|
3763
|
+
if (cached !== void 0) return cached;
|
|
3764
|
+
let result;
|
|
3765
|
+
try {
|
|
3766
|
+
const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
|
|
3767
|
+
result = {
|
|
3768
|
+
status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
|
|
3769
|
+
ok: true
|
|
3770
|
+
};
|
|
3771
|
+
} catch (err) {
|
|
3772
|
+
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3773
|
+
tags: {
|
|
3774
|
+
deviceId,
|
|
3775
|
+
capName: cap
|
|
3776
|
+
},
|
|
3777
|
+
meta: {
|
|
3778
|
+
sourceCap: srcCap,
|
|
3779
|
+
sourceId: srcId,
|
|
3780
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3781
|
+
}
|
|
3782
|
+
});
|
|
3783
|
+
result = {
|
|
3784
|
+
status: void 0,
|
|
3785
|
+
ok: false
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
statusMemo.set(key, result);
|
|
3789
|
+
return result;
|
|
3790
|
+
};
|
|
3591
3791
|
const resolved = [];
|
|
3592
3792
|
for (const link of links) {
|
|
3593
3793
|
const src = link.source;
|
|
@@ -3598,27 +3798,46 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3598
3798
|
});
|
|
3599
3799
|
continue;
|
|
3600
3800
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3801
|
+
if (src.kind === "expression") {
|
|
3802
|
+
const bindingValues = {};
|
|
3803
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3804
|
+
if (b.kind === "literal") {
|
|
3805
|
+
bindingValues[name] = b.value;
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
|
|
3809
|
+
if (bId === null) {
|
|
3810
|
+
bindingValues[name] = null;
|
|
3811
|
+
continue;
|
|
3812
|
+
}
|
|
3813
|
+
bindingValues[name] = (0, _camstack_types.toExpressionValue)((0, _camstack_types.getByPath)((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
|
|
3814
|
+
}
|
|
3815
|
+
const result = (0, _camstack_types.evaluateLinkExpression)(src.expr, bindingValues, now);
|
|
3816
|
+
if (result.ok) resolved.push({
|
|
3607
3817
|
link,
|
|
3608
|
-
sourceValue: (0, _camstack_types.applyTransform)(
|
|
3818
|
+
sourceValue: (0, _camstack_types.applyTransform)(result.value, link.transform)
|
|
3609
3819
|
});
|
|
3610
|
-
|
|
3611
|
-
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3820
|
+
else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
|
|
3612
3821
|
tags: {
|
|
3613
3822
|
deviceId,
|
|
3614
3823
|
capName: cap
|
|
3615
3824
|
},
|
|
3616
3825
|
meta: {
|
|
3617
|
-
|
|
3618
|
-
error:
|
|
3826
|
+
linkId: link.id,
|
|
3827
|
+
error: result.error
|
|
3619
3828
|
}
|
|
3620
3829
|
});
|
|
3830
|
+
continue;
|
|
3621
3831
|
}
|
|
3832
|
+
const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
|
|
3833
|
+
if (srcId === null) continue;
|
|
3834
|
+
const read = await readSourceStatus(srcId, src.cap);
|
|
3835
|
+
if (!read.ok) continue;
|
|
3836
|
+
const raw = (0, _camstack_types.getByPath)(read.status, src.fieldPath);
|
|
3837
|
+
resolved.push({
|
|
3838
|
+
link,
|
|
3839
|
+
sourceValue: (0, _camstack_types.applyTransform)(raw, link.transform)
|
|
3840
|
+
});
|
|
3622
3841
|
}
|
|
3623
3842
|
if (resolved.length === 0) return null;
|
|
3624
3843
|
const synthesizing = !isRecord$1(baseStatus);
|
|
@@ -3931,6 +4150,9 @@ var DeviceManagerAddon = class extends _camstack_types.BaseAddon {
|
|
|
3931
4150
|
setChildLayout: (input) => setChildLayout(pctx, input),
|
|
3932
4151
|
setDeviceLinks: (input) => setDeviceLinks(pctx, input),
|
|
3933
4152
|
setRole: (input) => setRole(pctx, input),
|
|
4153
|
+
setDisplay: (input) => setDisplay(pctx, input),
|
|
4154
|
+
getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
|
|
4155
|
+
setRoleDisplayDefaults: (input) => setRoleDisplayDefaults(pctx, input),
|
|
3934
4156
|
applyInitialMeta: (input) => applyInitialMeta(pctx, input),
|
|
3935
4157
|
setMetadata: (input) => setMetadata(pctx, input),
|
|
3936
4158
|
setDisabled: (input) => setDisabled(pctx, input),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
2
|
-
import { ALL_CAPABILITY_DEFINITIONS, BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceRole, DeviceStatusSchema, DeviceType, EventCategory, STREAM_PROFILE_META, WELL_KNOWN_TAB_MAP, applyTransform, buildStreamParamsConfigSchema, deviceManagerCapability, deviceStateCapability, deviceStatusCapability, enumerateItemArrayFields, enumerateSchemaFields, errMsg, getByPath, isDeviceConfigCap, parseStreamParamsFormPatch, setByPath, sleep } from "@camstack/types";
|
|
2
|
+
import { ALL_CAPABILITY_DEFINITIONS, BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceRole, DeviceStatusSchema, DeviceType, EventCategory, STREAM_PROFILE_META, WELL_KNOWN_TAB_MAP, applyTransform, buildStreamParamsConfigSchema, deviceManagerCapability, deviceStateCapability, deviceStatusCapability, enumerateItemArrayFields, enumerateSchemaFields, errMsg, evaluateLinkExpression, getByPath, isDeviceConfigCap, normalizeUnit, parseStreamParamsFormPatch, setByPath, sleep, toExpressionValue, validateExpressionSource } from "@camstack/types";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
/**
|
|
@@ -295,7 +295,8 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
|
|
|
295
295
|
...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
|
|
296
296
|
...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
|
|
297
297
|
...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
|
|
298
|
-
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {}
|
|
298
|
+
...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
|
|
299
|
+
...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
|
|
299
300
|
};
|
|
300
301
|
}
|
|
301
302
|
function resolveDeviceById(registry, deviceId) {
|
|
@@ -1366,20 +1367,41 @@ var DeviceEventPropagator = class {
|
|
|
1366
1367
|
function nodeKey(deviceId, cap) {
|
|
1367
1368
|
return `${deviceId}:${cap}`;
|
|
1368
1369
|
}
|
|
1370
|
+
/** The target's container stableId — its parent's (falling back to its own for
|
|
1371
|
+
* a top-level target). Sibling FIELD sources resolve relative to this, the same
|
|
1372
|
+
* rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
|
|
1373
|
+
function containerStableId(targetRow, rowById) {
|
|
1374
|
+
return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
|
|
1375
|
+
}
|
|
1376
|
+
/** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
|
|
1377
|
+
* null when it is a literal or resolves to no known device. */
|
|
1378
|
+
function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
|
|
1379
|
+
if (binding.kind === "literal") return null;
|
|
1380
|
+
const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
|
|
1381
|
+
const srcId = idByStableId.get(wantedStableId);
|
|
1382
|
+
if (srcId === void 0) return null;
|
|
1383
|
+
return nodeKey(srcId, binding.cap);
|
|
1384
|
+
}
|
|
1369
1385
|
/**
|
|
1370
|
-
* Resolve a link source to
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1386
|
+
* Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
|
|
1387
|
+
* A literal source depends on nothing; a field/global source depends on exactly
|
|
1388
|
+
* one node; an EXPRESSION source depends on one node per resolvable non-literal
|
|
1389
|
+
* binding. Unresolvable (dangling) bindings contribute no edge — they cannot
|
|
1390
|
+
* loop while dangling; a cycle that only becomes resolvable later is bounded by
|
|
1391
|
+
* the resolve-time re-entrancy guard.
|
|
1375
1392
|
*/
|
|
1376
|
-
function
|
|
1393
|
+
function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
|
|
1377
1394
|
const src = link.source;
|
|
1378
|
-
if (src.kind === "
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1395
|
+
if (src.kind === "expression") {
|
|
1396
|
+
const nodes = [];
|
|
1397
|
+
for (const binding of Object.values(src.bindings)) {
|
|
1398
|
+
const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
|
|
1399
|
+
if (node !== null) nodes.push(node);
|
|
1400
|
+
}
|
|
1401
|
+
return nodes;
|
|
1402
|
+
}
|
|
1403
|
+
const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
|
|
1404
|
+
return node !== null ? [node] : [];
|
|
1383
1405
|
}
|
|
1384
1406
|
/**
|
|
1385
1407
|
* Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
|
|
@@ -1397,11 +1419,11 @@ function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
|
|
|
1397
1419
|
const dependsOn = /* @__PURE__ */ new Map();
|
|
1398
1420
|
const addEdges = (row, links) => {
|
|
1399
1421
|
for (const link of links) {
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1422
|
+
const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
|
|
1423
|
+
if (srcNodes.length === 0) continue;
|
|
1402
1424
|
const tNode = nodeKey(row.id, link.target.cap);
|
|
1403
1425
|
const list = dependsOn.get(tNode) ?? [];
|
|
1404
|
-
list.push(srcNode);
|
|
1426
|
+
for (const srcNode of srcNodes) list.push(srcNode);
|
|
1405
1427
|
dependsOn.set(tNode, list);
|
|
1406
1428
|
}
|
|
1407
1429
|
};
|
|
@@ -1468,7 +1490,7 @@ function dfsFindCycle(root, dependsOn) {
|
|
|
1468
1490
|
* removeDevice), config persistence (persistConfig, loadConfig), the meta
|
|
1469
1491
|
* surface load (loadMeta, loadRuntimeState), every meta setter (setName,
|
|
1470
1492
|
* setLocation, setType, setIntegrationId, setLinkDeviceId,
|
|
1471
|
-
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole,
|
|
1493
|
+
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
|
|
1472
1494
|
* applyInitialMeta, setMetadata, setDisabled), and the location registry
|
|
1473
1495
|
* (listLocations, addLocation, removeLocation).
|
|
1474
1496
|
*
|
|
@@ -1577,6 +1599,7 @@ async function registerDevice(pctx, input) {
|
|
|
1577
1599
|
...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
|
|
1578
1600
|
...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
|
|
1579
1601
|
...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
|
|
1602
|
+
...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
|
|
1580
1603
|
parentDeviceId,
|
|
1581
1604
|
id,
|
|
1582
1605
|
features: featuresArr,
|
|
@@ -2018,6 +2041,11 @@ async function setDeviceLinks(pctx, input) {
|
|
|
2018
2041
|
const { deviceId, deviceLinks } = input;
|
|
2019
2042
|
const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
|
|
2020
2043
|
if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
|
|
2044
|
+
for (const link of deviceLinks) {
|
|
2045
|
+
if (link.source.kind !== "expression") continue;
|
|
2046
|
+
const err = validateExpressionSource(link.source);
|
|
2047
|
+
if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
|
|
2048
|
+
}
|
|
2021
2049
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2022
2050
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2023
2051
|
if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
|
|
@@ -2088,6 +2116,84 @@ async function setRole(pctx, input) {
|
|
|
2088
2116
|
});
|
|
2089
2117
|
}
|
|
2090
2118
|
/**
|
|
2119
|
+
* Normalize an operator-authored display override at write time so the render
|
|
2120
|
+
* path's `UNIT_TABLE` lookups always hit canonical spellings — maps `unit` and
|
|
2121
|
+
* every `perCap[*].unit` through `normalizeUnit`. Pure: rebuilds new objects,
|
|
2122
|
+
* never mutates the input. A unit `normalizeUnit` cannot canonicalize is left
|
|
2123
|
+
* as-is (the render path refuses conversion for unknown spellings).
|
|
2124
|
+
*/
|
|
2125
|
+
function normalizeDisplayOverride(display) {
|
|
2126
|
+
return {
|
|
2127
|
+
...display,
|
|
2128
|
+
...display.unit !== void 0 ? { unit: normalizeUnit(display.unit) ?? display.unit } : {},
|
|
2129
|
+
...display.perCap !== void 0 ? { perCap: Object.fromEntries(Object.entries(display.perCap).map(([cap, refine]) => [cap, refine.unit !== void 0 ? {
|
|
2130
|
+
...refine,
|
|
2131
|
+
unit: normalizeUnit(refine.unit) ?? refine.unit
|
|
2132
|
+
} : refine])) } : {}
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Set (or clear) the per-device display override on a device's meta row.
|
|
2137
|
+
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
2138
|
+
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
2139
|
+
* projection contract, never persists `display: undefined`). Override units are
|
|
2140
|
+
* normalized at write so the render path always looks up canonical spellings.
|
|
2141
|
+
*/
|
|
2142
|
+
async function setDisplay(pctx, input) {
|
|
2143
|
+
const { deviceId, display } = input;
|
|
2144
|
+
const normalized = display === null ? null : normalizeDisplayOverride(display);
|
|
2145
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2146
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2147
|
+
if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
|
|
2148
|
+
const { meta: m } = persisted;
|
|
2149
|
+
const key = String(deviceId);
|
|
2150
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2151
|
+
const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
|
|
2152
|
+
...m,
|
|
2153
|
+
display: normalized
|
|
2154
|
+
};
|
|
2155
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2156
|
+
...allMeta,
|
|
2157
|
+
[key]: nextRow
|
|
2158
|
+
} });
|
|
2159
|
+
});
|
|
2160
|
+
pctx.host.ctx.eventBus.emit({
|
|
2161
|
+
id: randomUUID(),
|
|
2162
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2163
|
+
source: {
|
|
2164
|
+
type: "device",
|
|
2165
|
+
id: deviceId
|
|
2166
|
+
},
|
|
2167
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2168
|
+
data: {
|
|
2169
|
+
deviceId,
|
|
2170
|
+
field: "display",
|
|
2171
|
+
value: normalized
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Read the operator-authored per-role display defaults. Empty record when none
|
|
2177
|
+
* set. Not per-device — a plain top-level-key read.
|
|
2178
|
+
*/
|
|
2179
|
+
async function getRoleDisplayDefaults(pctx, _input) {
|
|
2180
|
+
return { defaults: (await pctx.metaStore.readStore()).roleDisplayDefaults ?? {} };
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Replace the per-role display defaults whole-record (full replace). Override
|
|
2184
|
+
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
2185
|
+
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
2186
|
+
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
2187
|
+
* is emitted; the UI invalidates its own query on mutate.
|
|
2188
|
+
*/
|
|
2189
|
+
async function setRoleDisplayDefaults(pctx, input) {
|
|
2190
|
+
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
2191
|
+
...def,
|
|
2192
|
+
unit: normalizeUnit(def.unit) ?? def.unit
|
|
2193
|
+
} : def]));
|
|
2194
|
+
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2091
2197
|
* Batched meta pre-seed. Applies every provided field to the
|
|
2092
2198
|
* device's meta row in ONE read-modify-write under a single
|
|
2093
2199
|
* `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
|
|
@@ -2332,23 +2438,33 @@ async function removeLocation(pctx, input) {
|
|
|
2332
2438
|
function buildLinkIndexes(entries) {
|
|
2333
2439
|
const targets = /* @__PURE__ */ new Map();
|
|
2334
2440
|
const dependents = /* @__PURE__ */ new Map();
|
|
2335
|
-
for (const { targetDeviceId, link, sourceDeviceId } of entries) {
|
|
2441
|
+
for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
|
|
2336
2442
|
const tKey = `${targetDeviceId}:${link.target.cap}`;
|
|
2337
2443
|
const tList = targets.get(tKey) ?? [];
|
|
2338
2444
|
tList.push({
|
|
2339
2445
|
link,
|
|
2340
|
-
sourceDeviceId
|
|
2446
|
+
sourceDeviceId,
|
|
2447
|
+
...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
|
|
2341
2448
|
});
|
|
2342
2449
|
targets.set(tKey, tList);
|
|
2343
|
-
|
|
2344
|
-
const sKey = `${
|
|
2450
|
+
const addDependent = (sourceId, sourceCap) => {
|
|
2451
|
+
const sKey = `${sourceId}:${sourceCap}`;
|
|
2345
2452
|
const sList = dependents.get(sKey) ?? [];
|
|
2346
2453
|
sList.push({
|
|
2347
2454
|
targetDeviceId,
|
|
2348
2455
|
targetCap: link.target.cap
|
|
2349
2456
|
});
|
|
2350
2457
|
dependents.set(sKey, sList);
|
|
2458
|
+
};
|
|
2459
|
+
if (link.source.kind === "expression") {
|
|
2460
|
+
if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
|
|
2461
|
+
const binding = link.source.bindings[name];
|
|
2462
|
+
if (binding === void 0 || binding.kind === "literal") continue;
|
|
2463
|
+
addDependent(bindingDeviceId, binding.cap);
|
|
2464
|
+
}
|
|
2465
|
+
continue;
|
|
2351
2466
|
}
|
|
2467
|
+
if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
|
|
2352
2468
|
}
|
|
2353
2469
|
return {
|
|
2354
2470
|
targets,
|
|
@@ -2503,6 +2619,23 @@ var DeviceMetaStore = class {
|
|
|
2503
2619
|
if (container === void 0) continue;
|
|
2504
2620
|
for (const link of targetMeta.deviceLinks ?? []) {
|
|
2505
2621
|
const src = link.source;
|
|
2622
|
+
if (src.kind === "expression") {
|
|
2623
|
+
const bindingSourceIds = {};
|
|
2624
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
2625
|
+
if (b.kind === "literal") continue;
|
|
2626
|
+
const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
|
|
2627
|
+
expectedSources.add(wanted);
|
|
2628
|
+
const bid = idByStableId.get(wanted);
|
|
2629
|
+
if (bid !== void 0) bindingSourceIds[name] = bid;
|
|
2630
|
+
}
|
|
2631
|
+
entries.push({
|
|
2632
|
+
targetDeviceId: targetId,
|
|
2633
|
+
link,
|
|
2634
|
+
sourceDeviceId: -1,
|
|
2635
|
+
bindingSourceIds
|
|
2636
|
+
});
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2506
2639
|
if (src.kind === "literal") {
|
|
2507
2640
|
entries.push({
|
|
2508
2641
|
targetDeviceId: targetId,
|
|
@@ -2620,6 +2753,7 @@ async function listAll(pctx, input) {
|
|
|
2620
2753
|
...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2621
2754
|
...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2622
2755
|
...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2756
|
+
...m?.display !== void 0 ? { display: m.display } : {},
|
|
2623
2757
|
...(() => {
|
|
2624
2758
|
const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
2625
2759
|
return si !== void 0 ? { sourceInfo: si } : {};
|
|
@@ -2668,6 +2802,7 @@ async function getDevice(pctx, input) {
|
|
|
2668
2802
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2669
2803
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2670
2804
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2805
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2671
2806
|
...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
|
|
2672
2807
|
};
|
|
2673
2808
|
}
|
|
@@ -2730,6 +2865,7 @@ async function getChildren(pctx, input) {
|
|
|
2730
2865
|
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
2731
2866
|
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
2732
2867
|
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
2868
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
2733
2869
|
...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
|
|
2734
2870
|
});
|
|
2735
2871
|
}
|
|
@@ -3069,7 +3205,37 @@ function isRecord(x) {
|
|
|
3069
3205
|
* `source.kind` directly.
|
|
3070
3206
|
*/
|
|
3071
3207
|
function resolveLinkValue(link, readField) {
|
|
3072
|
-
|
|
3208
|
+
const src = link.source;
|
|
3209
|
+
if (src.kind === "expression") return void 0;
|
|
3210
|
+
return applyTransform(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Sync-channel expression resolution: bindings read from the mirror via
|
|
3214
|
+
* `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
|
|
3215
|
+
* on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
|
|
3216
|
+
* binding id (dangling source) or a non-primitive read resolves to `null`.
|
|
3217
|
+
* Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
|
|
3218
|
+
* the mirror path is hot and churn-free; the async channel logs. Compile
|
|
3219
|
+
* failures cost one parse ever thanks to the negative LRU compile cache. Pure.
|
|
3220
|
+
*/
|
|
3221
|
+
function resolveExpressionLinkValue(rl, readField, now) {
|
|
3222
|
+
const src = rl.link.source;
|
|
3223
|
+
if (src.kind !== "expression") return void 0;
|
|
3224
|
+
const bindingValues = {};
|
|
3225
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3226
|
+
if (b.kind === "literal") {
|
|
3227
|
+
bindingValues[name] = b.value;
|
|
3228
|
+
continue;
|
|
3229
|
+
}
|
|
3230
|
+
const bId = rl.bindingSourceIds?.[name];
|
|
3231
|
+
if (bId === void 0) {
|
|
3232
|
+
bindingValues[name] = null;
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
bindingValues[name] = toExpressionValue(readField(bId, b.cap, b.fieldPath)) ?? null;
|
|
3236
|
+
}
|
|
3237
|
+
const result = evaluateLinkExpression(src.expr, bindingValues, now);
|
|
3238
|
+
return result.ok ? applyTransform(result.value, rl.link.transform) : void 0;
|
|
3073
3239
|
}
|
|
3074
3240
|
/** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
|
|
3075
3241
|
* concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
|
|
@@ -3390,9 +3556,10 @@ var DeviceStateMirror = class DeviceStateMirror {
|
|
|
3390
3556
|
const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
|
|
3391
3557
|
const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
|
|
3392
3558
|
if (!links || links.length === 0) return raw ? { ...raw } : null;
|
|
3559
|
+
const now = Date.now();
|
|
3393
3560
|
const resolved = links.map((rl) => ({
|
|
3394
3561
|
link: rl.link,
|
|
3395
|
-
sourceValue: resolveLinkValue(rl.link, (srcCap, fieldPath) => getByPath(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3562
|
+
sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => getByPath(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => getByPath(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
|
|
3396
3563
|
}));
|
|
3397
3564
|
const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
|
|
3398
3565
|
const schema = capStatus?.schema;
|
|
@@ -3583,6 +3750,39 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3583
3750
|
const parentMeta = allMeta[String(parentId)];
|
|
3584
3751
|
if (parentMeta) containerStableId = parentMeta.stableId;
|
|
3585
3752
|
}
|
|
3753
|
+
const now = Date.now();
|
|
3754
|
+
const statusMemo = /* @__PURE__ */ new Map();
|
|
3755
|
+
const readSourceStatus = async (srcId, srcCap) => {
|
|
3756
|
+
const key = `${srcId}:${srcCap}`;
|
|
3757
|
+
const cached = statusMemo.get(key);
|
|
3758
|
+
if (cached !== void 0) return cached;
|
|
3759
|
+
let result;
|
|
3760
|
+
try {
|
|
3761
|
+
const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
|
|
3762
|
+
result = {
|
|
3763
|
+
status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
|
|
3764
|
+
ok: true
|
|
3765
|
+
};
|
|
3766
|
+
} catch (err) {
|
|
3767
|
+
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3768
|
+
tags: {
|
|
3769
|
+
deviceId,
|
|
3770
|
+
capName: cap
|
|
3771
|
+
},
|
|
3772
|
+
meta: {
|
|
3773
|
+
sourceCap: srcCap,
|
|
3774
|
+
sourceId: srcId,
|
|
3775
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
result = {
|
|
3779
|
+
status: void 0,
|
|
3780
|
+
ok: false
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
statusMemo.set(key, result);
|
|
3784
|
+
return result;
|
|
3785
|
+
};
|
|
3586
3786
|
const resolved = [];
|
|
3587
3787
|
for (const link of links) {
|
|
3588
3788
|
const src = link.source;
|
|
@@ -3593,27 +3793,46 @@ async function resolveLinkedStatusInner(pctx, input) {
|
|
|
3593
3793
|
});
|
|
3594
3794
|
continue;
|
|
3595
3795
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3796
|
+
if (src.kind === "expression") {
|
|
3797
|
+
const bindingValues = {};
|
|
3798
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3799
|
+
if (b.kind === "literal") {
|
|
3800
|
+
bindingValues[name] = b.value;
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
|
|
3804
|
+
if (bId === null) {
|
|
3805
|
+
bindingValues[name] = null;
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
bindingValues[name] = toExpressionValue(getByPath((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
|
|
3809
|
+
}
|
|
3810
|
+
const result = evaluateLinkExpression(src.expr, bindingValues, now);
|
|
3811
|
+
if (result.ok) resolved.push({
|
|
3602
3812
|
link,
|
|
3603
|
-
sourceValue: applyTransform(
|
|
3813
|
+
sourceValue: applyTransform(result.value, link.transform)
|
|
3604
3814
|
});
|
|
3605
|
-
|
|
3606
|
-
pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
|
|
3815
|
+
else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
|
|
3607
3816
|
tags: {
|
|
3608
3817
|
deviceId,
|
|
3609
3818
|
capName: cap
|
|
3610
3819
|
},
|
|
3611
3820
|
meta: {
|
|
3612
|
-
|
|
3613
|
-
error:
|
|
3821
|
+
linkId: link.id,
|
|
3822
|
+
error: result.error
|
|
3614
3823
|
}
|
|
3615
3824
|
});
|
|
3825
|
+
continue;
|
|
3616
3826
|
}
|
|
3827
|
+
const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
|
|
3828
|
+
if (srcId === null) continue;
|
|
3829
|
+
const read = await readSourceStatus(srcId, src.cap);
|
|
3830
|
+
if (!read.ok) continue;
|
|
3831
|
+
const raw = getByPath(read.status, src.fieldPath);
|
|
3832
|
+
resolved.push({
|
|
3833
|
+
link,
|
|
3834
|
+
sourceValue: applyTransform(raw, link.transform)
|
|
3835
|
+
});
|
|
3617
3836
|
}
|
|
3618
3837
|
if (resolved.length === 0) return null;
|
|
3619
3838
|
const synthesizing = !isRecord$1(baseStatus);
|
|
@@ -3926,6 +4145,9 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
3926
4145
|
setChildLayout: (input) => setChildLayout(pctx, input),
|
|
3927
4146
|
setDeviceLinks: (input) => setDeviceLinks(pctx, input),
|
|
3928
4147
|
setRole: (input) => setRole(pctx, input),
|
|
4148
|
+
setDisplay: (input) => setDisplay(pctx, input),
|
|
4149
|
+
getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
|
|
4150
|
+
setRoleDisplayDefaults: (input) => setRoleDisplayDefaults(pctx, input),
|
|
3929
4151
|
applyInitialMeta: (input) => applyInitialMeta(pctx, input),
|
|
3930
4152
|
setMetadata: (input) => setMetadata(pctx, input),
|
|
3931
4153
|
setDisabled: (input) => setDisabled(pctx, input),
|
|
@@ -97,6 +97,27 @@ export declare function setDeviceLinks(pctx: ProviderContext, input: Parameters<
|
|
|
97
97
|
* `setIntegrationId`). Idempotent. `null` clears a previous role.
|
|
98
98
|
*/
|
|
99
99
|
export declare function setRole(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setRole']>[0]): ReturnType<IDeviceManagerProvider['setRole']>;
|
|
100
|
+
/**
|
|
101
|
+
* Set (or clear) the per-device display override on a device's meta row.
|
|
102
|
+
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
103
|
+
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
104
|
+
* projection contract, never persists `display: undefined`). Override units are
|
|
105
|
+
* normalized at write so the render path always looks up canonical spellings.
|
|
106
|
+
*/
|
|
107
|
+
export declare function setDisplay(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setDisplay']>[0]): ReturnType<IDeviceManagerProvider['setDisplay']>;
|
|
108
|
+
/**
|
|
109
|
+
* Read the operator-authored per-role display defaults. Empty record when none
|
|
110
|
+
* set. Not per-device — a plain top-level-key read.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getRoleDisplayDefaults(pctx: ProviderContext, _input: Parameters<IDeviceManagerProvider['getRoleDisplayDefaults']>[0]): ReturnType<IDeviceManagerProvider['getRoleDisplayDefaults']>;
|
|
113
|
+
/**
|
|
114
|
+
* Replace the per-role display defaults whole-record (full replace). Override
|
|
115
|
+
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
116
|
+
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
117
|
+
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
118
|
+
* is emitted; the UI invalidates its own query on mutate.
|
|
119
|
+
*/
|
|
120
|
+
export declare function setRoleDisplayDefaults(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setRoleDisplayDefaults']>[0]): ReturnType<IDeviceManagerProvider['setRoleDisplayDefaults']>;
|
|
100
121
|
/**
|
|
101
122
|
* Batched meta pre-seed. Applies every provided field to the
|
|
102
123
|
* device's meta row in ONE read-modify-write under a single
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChildLayout, DeviceLinks } from '@camstack/types';
|
|
1
|
+
import { ChildLayout, DeviceDisplayOverride, DeviceLinks, 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
|
|
@@ -132,6 +132,11 @@ export interface PersistedDeviceMeta {
|
|
|
132
132
|
* Optional: only present for accessory children that carry a known role
|
|
133
133
|
* (e.g. 'numeric-sensor', 'binary-sensor'). `null` clears a previous role. */
|
|
134
134
|
role?: string | null;
|
|
135
|
+
/** Operator-authored per-device display override (icon/label/unit/precision/
|
|
136
|
+
* hidden), set via `setDisplay`. Same create/persist/project/restore
|
|
137
|
+
* lifecycle as `deviceLinks`. Absent ⇒ no override. Applied at render time
|
|
138
|
+
* only — storage stays in source units. */
|
|
139
|
+
display?: DeviceDisplayOverride;
|
|
135
140
|
}
|
|
136
141
|
export interface AddonStore {
|
|
137
142
|
deviceIndex?: Record<string, string[]>;
|
|
@@ -166,4 +171,10 @@ export interface AddonStore {
|
|
|
166
171
|
* the operator forgot to register a label before assigning it.
|
|
167
172
|
*/
|
|
168
173
|
locations?: readonly string[];
|
|
174
|
+
/** Operator-authored per-role display defaults (unit/precision/icon), keyed
|
|
175
|
+
* by `DeviceRole` string. Resolution merges these UNDER any per-device
|
|
176
|
+
* `display` override. Written whole-record by `setRoleDisplayDefaults`
|
|
177
|
+
* (full replace — no read-modify-write, single writer, so it never
|
|
178
|
+
* interacts with the `deviceMeta` write lock). */
|
|
179
|
+
roleDisplayDefaults?: Record<string, RoleDisplayDefault>;
|
|
169
180
|
}
|
|
@@ -47,6 +47,7 @@ export declare function isDeviceRole(value: string): value is DeviceRole;
|
|
|
47
47
|
* never leaks an off-enum string onto the wire shape. */
|
|
48
48
|
export declare function toDeviceRole(value: string | null | undefined): DeviceRole | null;
|
|
49
49
|
export declare function toDeviceInfo(addonId: string, device: IDevice, metadata?: Record<string, unknown> | null, metaRow?: PersistedDeviceMeta | null): {
|
|
50
|
+
display?: import('@camstack/types').DeviceDisplayOverride | undefined;
|
|
50
51
|
deviceLinks?: import('@camstack/types').DeviceLinks | undefined;
|
|
51
52
|
childLayout?: import('@camstack/types').ChildLayout | undefined;
|
|
52
53
|
primaryChildEntityId?: string | null | undefined;
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ const require_builtins_local_auth_local_auth_addon = require("./builtins/local-a
|
|
|
20
20
|
require("./builtins/local-auth/index.js");
|
|
21
21
|
const require_builtins_device_manager_device_manager_addon = require("./builtins/device-manager/device-manager.addon.js");
|
|
22
22
|
require("./builtins/device-manager/index.js");
|
|
23
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
23
|
+
const require_manifest_python_deps = require("./manifest-python-deps-XWJwKYDx.js");
|
|
24
24
|
const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
|
|
25
25
|
let _camstack_types_node = require("@camstack/types/node");
|
|
26
26
|
let node_http = require("node:http");
|
package/dist/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { LocalAuthAddon, a as require_ms, c as __esmMin, d as __toCommonJS, f as
|
|
|
18
18
|
import "./builtins/local-auth/index.mjs";
|
|
19
19
|
import { DeviceManagerAddon } from "./builtins/device-manager/device-manager.addon.mjs";
|
|
20
20
|
import "./builtins/device-manager/index.mjs";
|
|
21
|
-
import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as resolveHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as resolveAddonClass, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getMoleculerEventStats, dt as capBareAction, et as buildUdsNativeCapProxy, f as registerEventBusService, ft as capServiceName, g as createKernelHwAccel, gt as DeviceRegistry, h as AddonDepsManager, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as subscribePassthrough, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as setNodeEventInterest, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as getBrokerEventBus, ut as capActionSuffix, v as createHwAccelService, vt as CapabilityUnavailableError, w as buildLinkChain, x as getCapUsageRegistry, y as CapUsageRegistry, yt as installManifestNativeDeps, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-
|
|
21
|
+
import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as resolveHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as resolveAddonClass, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getMoleculerEventStats, dt as capBareAction, et as buildUdsNativeCapProxy, f as registerEventBusService, ft as capServiceName, g as createKernelHwAccel, gt as DeviceRegistry, h as AddonDepsManager, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as subscribePassthrough, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as setNodeEventInterest, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as getBrokerEventBus, ut as capActionSuffix, v as createHwAccelService, vt as CapabilityUnavailableError, w as buildLinkChain, x as getCapUsageRegistry, y as CapUsageRegistry, yt as installManifestNativeDeps, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-CPJXzrZt.mjs";
|
|
22
22
|
import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
|
|
23
23
|
import { PYTHON_VERSION, buildBinaryPath, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
|
|
24
24
|
import { request } from "node:http";
|
|
@@ -1423,6 +1423,18 @@ function createBrokerDeviceManagerApi(opts) {
|
|
|
1423
1423
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
1424
1424
|
});
|
|
1425
1425
|
});
|
|
1426
|
+
if (initialMeta.display !== void 0) await callDeviceManager(api, "setDisplay", {
|
|
1427
|
+
deviceId: id,
|
|
1428
|
+
display: initialMeta.display
|
|
1429
|
+
}).catch((err) => {
|
|
1430
|
+
opts.logger.warn("create: setDisplay pre-seed failed", {
|
|
1431
|
+
tags: {
|
|
1432
|
+
stableId,
|
|
1433
|
+
deviceId: id
|
|
1434
|
+
},
|
|
1435
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1426
1438
|
}
|
|
1427
1439
|
if (Object.keys(config).length > 0) await callDeviceManager(api, "persistConfig", {
|
|
1428
1440
|
deviceId: id,
|
|
@@ -1425,6 +1425,18 @@ function createBrokerDeviceManagerApi(opts) {
|
|
|
1425
1425
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
1426
1426
|
});
|
|
1427
1427
|
});
|
|
1428
|
+
if (initialMeta.display !== void 0) await callDeviceManager(api, "setDisplay", {
|
|
1429
|
+
deviceId: id,
|
|
1430
|
+
display: initialMeta.display
|
|
1431
|
+
}).catch((err) => {
|
|
1432
|
+
opts.logger.warn("create: setDisplay pre-seed failed", {
|
|
1433
|
+
tags: {
|
|
1434
|
+
stableId,
|
|
1435
|
+
deviceId: id
|
|
1436
|
+
},
|
|
1437
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1428
1440
|
}
|
|
1429
1441
|
if (Object.keys(config).length > 0) await callDeviceManager(api, "persistConfig", {
|
|
1430
1442
|
deviceId: id,
|