@camstack/system 1.1.50 → 1.1.52
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 +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- 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/device-manager/device-linked-devices.d.ts +47 -0
- package/dist/builtins/device-manager/device-manager.addon.js +1840 -1712
- package/dist/builtins/device-manager/device-manager.addon.mjs +1840 -1712
- package/dist/builtins/doorbell/index.d.ts +2 -0
- package/dist/builtins/doorbell/index.js +8 -0
- package/dist/builtins/doorbell/index.mjs +2 -0
- package/dist/builtins/doorbell/trigger-engine.d.ts +69 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +87 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +489 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +483 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- 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/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 +162 -12
- package/dist/builtins/snapshot/index.mjs +162 -12
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +38 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +26 -0
- 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 +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- 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 +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +4 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-CC3E-_3h.mjs → dist-B_PSvr-m.mjs} +237 -13
- package/dist/{dist-CQYo6Zp8.js → dist-BduBhv6N.js} +242 -12
- package/dist/http/file-data-plane.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -23
- package/dist/index.mjs +35 -24
- package/dist/kernel/addon-installer.d.ts +8 -0
- package/dist/kernel/deps/manifest-native-deps.d.ts +1 -1
- package/dist/kernel/deps/npm-command.d.ts +38 -0
- package/dist/{manifest-python-deps-DqATCsK0.mjs → manifest-python-deps-CIyvuHAB.mjs} +268 -124
- package/dist/{manifest-python-deps-Wy8dNJpr.js → manifest-python-deps-CPAMH5gV.js} +279 -123
- package/dist/{model-download-service-C-IHWnXx.mjs → model-download-service-Cp9f4dk6.mjs} +1 -1
- package/dist/{model-download-service-D-Umz4-L.js → model-download-service-hf0ookyy.js} +1 -1
- package/package.json +13 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as evaluateLinkExpression, J as setByPath, Ot as sleep, S as deviceStatusCapability, St as isDeviceConfigCap, T as enumerateSchemaFields, U as parseStreamParamsFormPatch, V as normalizeUnit, a as DeviceStatusSchema, b as deviceManagerCapability, c as STREAM_PROFILE_META, ct as DeviceFeature, et as toExpressionValue, g as applyTransform, i as CAP_NAMES_WITH_STATUS, it as BaseAddon, k as getByPath, kt as EventCategory, lt as DeviceRole, nt as validateExpressionSource, pt as WELL_KNOWN_TAB_MAP, rt as errMsg, t as ALL_CAPABILITY_DEFINITIONS, ut as DeviceType, w as enumerateItemArrayFields, x as deviceStateCapability, y as buildStreamParamsConfigSchema } from "../../dist-B_PSvr-m.mjs";
|
|
2
2
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -1742,1832 +1742,1956 @@ var DeviceEventPropagator = class {
|
|
|
1742
1742
|
}
|
|
1743
1743
|
};
|
|
1744
1744
|
//#endregion
|
|
1745
|
-
//#region src/builtins/device-manager/device-
|
|
1746
|
-
function nodeKey(deviceId, cap) {
|
|
1747
|
-
return `${deviceId}:${cap}`;
|
|
1748
|
-
}
|
|
1749
|
-
/** The target's container stableId — its parent's (falling back to its own for
|
|
1750
|
-
* a top-level target). Sibling FIELD sources resolve relative to this, the same
|
|
1751
|
-
* rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
|
|
1752
|
-
function containerStableId(targetRow, rowById) {
|
|
1753
|
-
return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
|
|
1754
|
-
}
|
|
1755
|
-
/** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
|
|
1756
|
-
* null when it is a literal or resolves to no known device. */
|
|
1757
|
-
function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
|
|
1758
|
-
if (binding.kind === "literal") return null;
|
|
1759
|
-
const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
|
|
1760
|
-
const srcId = idByStableId.get(wantedStableId);
|
|
1761
|
-
if (srcId === void 0) return null;
|
|
1762
|
-
return nodeKey(srcId, binding.cap);
|
|
1763
|
-
}
|
|
1745
|
+
//#region src/builtins/device-manager/device-queries.ts
|
|
1764
1746
|
/**
|
|
1765
|
-
*
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
1770
|
-
* the
|
|
1747
|
+
* Device query + live-runtime + provider/adoption operations for the
|
|
1748
|
+
* device-manager addon — the read/live half of the `IDeviceManagerProvider`
|
|
1749
|
+
* surface. Extracted verbatim from the `onInitialize` provider literal as free
|
|
1750
|
+
* functions taking a `ProviderContext` (DI). Input/return types are pinned to
|
|
1751
|
+
* the cap contract via `Parameters<IDeviceManagerProvider['x']>[0]` /
|
|
1752
|
+
* `ReturnType<…>` so the typed surface is identical to the inferred methods.
|
|
1753
|
+
*
|
|
1754
|
+
* Covers: persisted/live device queries (listPersistedByAddon, listAll,
|
|
1755
|
+
* getDevice, getChildren), live runtime methods (getStreamSources,
|
|
1756
|
+
* getConfigSchema, getSettingsSchema, updateConfig, enable, disable, remove,
|
|
1757
|
+
* removeByIntegration, getStreamProfileMap, setStreamProfileMap, probeStreams),
|
|
1758
|
+
* device-provider ops (discoverDevices, adoptDevice, getCreationSchema,
|
|
1759
|
+
* createDevice, testCreationField), and device-adoption ops
|
|
1760
|
+
* (adoptionListCandidates/Refresh/Adopt/Release/Resync, testField).
|
|
1761
|
+
*
|
|
1762
|
+
* The hub-live-then-persisted-fallback union, the cross-process device-ops
|
|
1763
|
+
* dispatch, the depth-first remove cascade, and the resetToSource resync purge
|
|
1764
|
+
* are byte-for-byte preserved. `remove`/`removeByIntegration`/`enable`/`disable`/
|
|
1765
|
+
* `probeStreams`/`getStreamProfileMap` self-call sibling cap methods via
|
|
1766
|
+
* `pctx.provider`. Pure refactor — no behavior change.
|
|
1771
1767
|
*/
|
|
1772
|
-
function
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
return
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1768
|
+
async function listPersistedByAddon(pctx, input) {
|
|
1769
|
+
const { addonId } = input;
|
|
1770
|
+
const [index, meta] = await Promise.all([pctx.metaStore.readIndex(), pctx.metaStore.readMeta()]);
|
|
1771
|
+
const stableIds = index[addonId] ?? [];
|
|
1772
|
+
const byStableId = /* @__PURE__ */ new Map();
|
|
1773
|
+
for (const m of Object.values(meta)) if (m.addonId === addonId) byStableId.set(m.stableId, m);
|
|
1774
|
+
return stableIds.map((stableId) => {
|
|
1775
|
+
const m = byStableId.get(stableId);
|
|
1776
|
+
return {
|
|
1777
|
+
id: m.id,
|
|
1778
|
+
stableId,
|
|
1779
|
+
type: m.type,
|
|
1780
|
+
name: m.name,
|
|
1781
|
+
location: m.location ?? null,
|
|
1782
|
+
disabled: m.disabled ?? false,
|
|
1783
|
+
parentDeviceId: m.parentDeviceId
|
|
1784
|
+
};
|
|
1785
|
+
});
|
|
1784
1786
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if (srcNodes.length === 0) continue;
|
|
1803
|
-
const tNode = nodeKey(row.id, link.target.cap);
|
|
1804
|
-
const list = dependsOn.get(tNode) ?? [];
|
|
1805
|
-
for (const srcNode of srcNodes) list.push(srcNode);
|
|
1806
|
-
dependsOn.set(tNode, list);
|
|
1787
|
+
async function listAll(pctx, input) {
|
|
1788
|
+
const { addonId } = input;
|
|
1789
|
+
const results = [];
|
|
1790
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1791
|
+
const meta = await pctx.metaStore.readMeta();
|
|
1792
|
+
const metadataMap = await pctx.metaStore.readMetadataMap();
|
|
1793
|
+
if (pctx.registry) {
|
|
1794
|
+
const liveEntries = addonId ? pctx.registry.getAllForAddon(addonId).map((device) => ({
|
|
1795
|
+
addonId,
|
|
1796
|
+
device
|
|
1797
|
+
})) : pctx.registry.getAllWithAddonId();
|
|
1798
|
+
for (const { addonId: aid, device } of liveEntries) {
|
|
1799
|
+
const key = String(device.id);
|
|
1800
|
+
const metadata = metadataMap[key] ?? null;
|
|
1801
|
+
const metaRow = meta[key] ?? null;
|
|
1802
|
+
results.push(toDeviceInfo(aid, device, metadata, metaRow));
|
|
1803
|
+
seen.add(key);
|
|
1807
1804
|
}
|
|
1808
|
-
};
|
|
1809
|
-
for (const r of rows) {
|
|
1810
|
-
const links = r.id === editedDeviceId ? editedLinks : r.deviceLinks ?? [];
|
|
1811
|
-
if (links.length > 0) addEdges(r, links);
|
|
1812
1805
|
}
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1806
|
+
const index = await pctx.metaStore.readIndex();
|
|
1807
|
+
const metaByAddonStable = /* @__PURE__ */ new Map();
|
|
1808
|
+
for (const m of Object.values(meta)) metaByAddonStable.set(`${m.addonId}${m.stableId}`, m);
|
|
1809
|
+
const targetAddons = addonId ? [addonId] : Object.keys(index);
|
|
1810
|
+
for (const aid of targetAddons) for (const stableId of index[aid] ?? []) {
|
|
1811
|
+
const m = metaByAddonStable.get(`${aid}${stableId}`);
|
|
1812
|
+
const key = String(m.id);
|
|
1813
|
+
if (seen.has(key)) continue;
|
|
1814
|
+
const persistedType = m.type;
|
|
1815
|
+
const persistedConfig = await pctx.settings.readDeviceStore(m.id);
|
|
1816
|
+
const metadata = metadataMap[key] ?? null;
|
|
1817
|
+
results.push({
|
|
1818
|
+
id: m.id,
|
|
1819
|
+
stableId,
|
|
1820
|
+
addonId: aid,
|
|
1821
|
+
type: persistedType,
|
|
1822
|
+
name: m?.name ?? stableId,
|
|
1823
|
+
location: m?.location ?? null,
|
|
1824
|
+
disabled: m?.disabled ?? false,
|
|
1825
|
+
parentDeviceId: m?.parentDeviceId ?? null,
|
|
1826
|
+
role: toDeviceRole(m?.role),
|
|
1827
|
+
online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
|
|
1828
|
+
probed: pctx.host.resolveDeviceProbed(m.id),
|
|
1829
|
+
features: persistedFeatures(m?.features),
|
|
1830
|
+
isCamera: persistedType === DeviceType.Camera,
|
|
1831
|
+
config: persistedConfig ?? {},
|
|
1832
|
+
metadata,
|
|
1833
|
+
...m?.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
1834
|
+
...m?.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
1835
|
+
...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
1836
|
+
...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
1837
|
+
...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
1838
|
+
...m?.display !== void 0 ? { display: m.display } : {},
|
|
1839
|
+
...(() => {
|
|
1840
|
+
const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
1841
|
+
return si !== void 0 ? { sourceInfo: si } : {};
|
|
1842
|
+
})()
|
|
1843
|
+
});
|
|
1817
1844
|
}
|
|
1818
|
-
return
|
|
1845
|
+
return results;
|
|
1819
1846
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
onPath.add(root);
|
|
1831
|
-
path.push(root);
|
|
1832
|
-
while (stack.length > 0) {
|
|
1833
|
-
const frame = stack[stack.length - 1];
|
|
1834
|
-
const children = dependsOn.get(frame.node) ?? [];
|
|
1835
|
-
if (frame.nextChild < children.length) {
|
|
1836
|
-
const child = children[frame.nextChild];
|
|
1837
|
-
frame.nextChild += 1;
|
|
1838
|
-
if (onPath.has(child)) {
|
|
1839
|
-
const start = path.indexOf(child);
|
|
1840
|
-
return [...path.slice(start), child];
|
|
1841
|
-
}
|
|
1842
|
-
if (done.has(child)) continue;
|
|
1843
|
-
stack.push({
|
|
1844
|
-
node: child,
|
|
1845
|
-
nextChild: 0
|
|
1846
|
-
});
|
|
1847
|
-
onPath.add(child);
|
|
1848
|
-
path.push(child);
|
|
1849
|
-
continue;
|
|
1847
|
+
async function getDevice(pctx, input) {
|
|
1848
|
+
const { deviceId } = input;
|
|
1849
|
+
if (pctx.registry) {
|
|
1850
|
+
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
1851
|
+
if (found) {
|
|
1852
|
+
const key = String(found.device.id);
|
|
1853
|
+
const [map, metaMap] = await Promise.all([pctx.metaStore.readMetadataMap(), pctx.metaStore.readMeta()]);
|
|
1854
|
+
const metadata = map[key] ?? null;
|
|
1855
|
+
const metaRow = metaMap[key] ?? null;
|
|
1856
|
+
return toDeviceInfo(found.addonId, found.device, metadata, metaRow);
|
|
1850
1857
|
}
|
|
1851
|
-
stack.pop();
|
|
1852
|
-
onPath.delete(frame.node);
|
|
1853
|
-
path.pop();
|
|
1854
|
-
done.add(frame.node);
|
|
1855
1858
|
}
|
|
1856
|
-
|
|
1859
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
1860
|
+
if (!persisted) return null;
|
|
1861
|
+
const { addonId: aid, stableId, meta: m } = persisted;
|
|
1862
|
+
const persistedConfig = await pctx.settings.readDeviceStore(m.id);
|
|
1863
|
+
const key = String(deviceId);
|
|
1864
|
+
const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
|
|
1865
|
+
const sourceInfoGetDevice = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
1866
|
+
return {
|
|
1867
|
+
id: deviceId,
|
|
1868
|
+
stableId,
|
|
1869
|
+
addonId: aid,
|
|
1870
|
+
type: m.type,
|
|
1871
|
+
name: m.name,
|
|
1872
|
+
location: m.location ?? null,
|
|
1873
|
+
disabled: m.disabled ?? false,
|
|
1874
|
+
parentDeviceId: m.parentDeviceId,
|
|
1875
|
+
role: toDeviceRole(m.role),
|
|
1876
|
+
online: pctx.host.resolveDeviceOnline(deviceId, true),
|
|
1877
|
+
probed: pctx.host.resolveDeviceProbed(deviceId),
|
|
1878
|
+
features: persistedFeatures(m.features),
|
|
1879
|
+
isCamera: false,
|
|
1880
|
+
config: persistedConfig ?? {},
|
|
1881
|
+
metadata,
|
|
1882
|
+
...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
1883
|
+
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
1884
|
+
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
1885
|
+
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
1886
|
+
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
1887
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
1888
|
+
...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
|
|
1889
|
+
};
|
|
1857
1890
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
* `setIntegrationId` cap handler AND the create/adopt forwarders (queries)
|
|
1885
|
-
* that stamp an integration's ownership onto every device it materializes.
|
|
1886
|
-
* Idempotent. Takes its low-level deps directly (not the full `ProviderContext`)
|
|
1887
|
-
* because `ProviderContext.stampIntegrationId` delegates HERE — passing the
|
|
1888
|
-
* context would be a capture cycle.
|
|
1889
|
-
*/
|
|
1890
|
-
async function stampIntegrationId(metaStore, settings, ctx, deviceId, integrationId) {
|
|
1891
|
-
await metaStore.withMetaWriteLock(async () => {
|
|
1892
|
-
const persisted = await metaStore.resolvePersistedById(deviceId);
|
|
1893
|
-
if (!persisted) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
|
|
1894
|
-
const { meta: m } = persisted;
|
|
1895
|
-
const key = String(deviceId);
|
|
1896
|
-
const allMeta = await metaStore.readMeta();
|
|
1897
|
-
await settings.writeAddonStore({ deviceMeta: {
|
|
1898
|
-
...allMeta,
|
|
1899
|
-
[key]: {
|
|
1900
|
-
...m,
|
|
1901
|
-
integrationId
|
|
1902
|
-
}
|
|
1903
|
-
} });
|
|
1904
|
-
});
|
|
1905
|
-
ctx.eventBus.emit({
|
|
1906
|
-
id: randomUUID(),
|
|
1907
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1908
|
-
source: {
|
|
1909
|
-
type: "device",
|
|
1910
|
-
id: deviceId
|
|
1911
|
-
},
|
|
1912
|
-
category: EventCategory.DeviceMetaChanged,
|
|
1913
|
-
data: {
|
|
1914
|
-
deviceId,
|
|
1915
|
-
field: "integrationId",
|
|
1916
|
-
value: integrationId
|
|
1891
|
+
async function getChildren(pctx, input) {
|
|
1892
|
+
const { parentDeviceId } = input;
|
|
1893
|
+
let ownerAddonId = null;
|
|
1894
|
+
if (pctx.registry) {
|
|
1895
|
+
if (pctx.registry.getById(parentDeviceId)) ownerAddonId = pctx.registry.getAddonId(parentDeviceId);
|
|
1896
|
+
}
|
|
1897
|
+
if (!ownerAddonId) {
|
|
1898
|
+
const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
|
|
1899
|
+
if (!persisted) return [];
|
|
1900
|
+
ownerAddonId = persisted.addonId;
|
|
1901
|
+
}
|
|
1902
|
+
const results = [];
|
|
1903
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1904
|
+
const [index, meta, metadataMap] = await Promise.all([
|
|
1905
|
+
pctx.metaStore.readIndex(),
|
|
1906
|
+
pctx.metaStore.readMeta(),
|
|
1907
|
+
pctx.metaStore.readMetadataMap()
|
|
1908
|
+
]);
|
|
1909
|
+
if (pctx.registry) {
|
|
1910
|
+
const liveChildren = pctx.registry.getChildren(parentDeviceId);
|
|
1911
|
+
for (const device of liveChildren) {
|
|
1912
|
+
const key = String(device.id);
|
|
1913
|
+
const metadata = metadataMap[key] ?? null;
|
|
1914
|
+
const metaRow = meta[key] ?? null;
|
|
1915
|
+
results.push(toDeviceInfo(ownerAddonId, device, metadata, metaRow));
|
|
1916
|
+
seen.add(key);
|
|
1917
1917
|
}
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
const
|
|
1924
|
-
const
|
|
1925
|
-
if (
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
...index,
|
|
1953
|
-
[addonId]: [...existing, stableId]
|
|
1954
|
-
} });
|
|
1955
|
-
const meta = await pctx.metaStore.readMeta();
|
|
1956
|
-
const existingMeta = meta[key];
|
|
1957
|
-
const wasUserNamed = existingMeta?.userNamed ?? (existingMeta !== void 0 && existingMeta.name !== stableId);
|
|
1958
|
-
const resolvedName = wasUserNamed && existingMeta !== void 0 ? existingMeta.name : name;
|
|
1959
|
-
const isFirst = !existingMeta || !wasInIndex;
|
|
1960
|
-
const fingerprint = canonicalDeviceFingerprint({
|
|
1961
|
-
deviceId: id,
|
|
1962
|
-
deviceType: type,
|
|
1963
|
-
features: featuresArr
|
|
1918
|
+
}
|
|
1919
|
+
const ownerMetaByStableId = /* @__PURE__ */ new Map();
|
|
1920
|
+
for (const m of Object.values(meta)) if (m.addonId === ownerAddonId) ownerMetaByStableId.set(m.stableId, m);
|
|
1921
|
+
const persistedChildren = (index[ownerAddonId] ?? []).filter((sid) => ownerMetaByStableId.get(sid)?.parentDeviceId === parentDeviceId);
|
|
1922
|
+
for (const childStableId of persistedChildren) {
|
|
1923
|
+
const m = ownerMetaByStableId.get(childStableId);
|
|
1924
|
+
const key = String(m.id);
|
|
1925
|
+
if (seen.has(key)) continue;
|
|
1926
|
+
const persistedConfig = await pctx.settings.readDeviceStore(m.id);
|
|
1927
|
+
const metadata = metadataMap[key] ?? null;
|
|
1928
|
+
const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig ?? {}, childStableId, ownerAddonId);
|
|
1929
|
+
results.push({
|
|
1930
|
+
id: m.id,
|
|
1931
|
+
stableId: childStableId,
|
|
1932
|
+
addonId: ownerAddonId,
|
|
1933
|
+
type: m.type,
|
|
1934
|
+
name: m.name,
|
|
1935
|
+
location: m.location ?? null,
|
|
1936
|
+
disabled: m.disabled ?? false,
|
|
1937
|
+
parentDeviceId,
|
|
1938
|
+
role: toDeviceRole(m.role),
|
|
1939
|
+
online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
|
|
1940
|
+
probed: pctx.host.resolveDeviceProbed(m.id),
|
|
1941
|
+
features: persistedFeatures(m.features),
|
|
1942
|
+
isCamera: false,
|
|
1943
|
+
config: persistedConfig ?? {},
|
|
1944
|
+
metadata,
|
|
1945
|
+
...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
1946
|
+
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
1947
|
+
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
1948
|
+
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
1949
|
+
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
1950
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
1951
|
+
...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
|
|
1964
1952
|
});
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
exportFingerprint: fingerprint
|
|
1986
|
-
}
|
|
1987
|
-
} });
|
|
1988
|
-
return {
|
|
1989
|
-
isFirstRegistration: isFirst,
|
|
1990
|
-
exportFingerprint: fingerprint,
|
|
1991
|
-
fingerprintChanged: existingMeta?.exportFingerprint !== fingerprint
|
|
1992
|
-
};
|
|
1993
|
-
});
|
|
1994
|
-
if (Object.keys(config).length > 0) await pctx.settings.writeDeviceStore(id, config);
|
|
1995
|
-
pctx.metaStore.idToAddonId.set(id, addonId);
|
|
1996
|
-
if (isFirstRegistration) pctx.host.ctx.eventBus.emit({
|
|
1997
|
-
id: randomUUID(),
|
|
1998
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1999
|
-
source: {
|
|
2000
|
-
type: "device",
|
|
2001
|
-
id
|
|
2002
|
-
},
|
|
2003
|
-
category: EventCategory.DeviceRegistered,
|
|
2004
|
-
data: {
|
|
2005
|
-
deviceId: id,
|
|
2006
|
-
name: name.length > 0 ? name : stableId,
|
|
2007
|
-
providerId: addonId,
|
|
2008
|
-
parentDeviceId: parentDeviceId ?? null
|
|
2009
|
-
}
|
|
2010
|
-
});
|
|
2011
|
-
else pctx.host.ctx.eventBus.emit({
|
|
2012
|
-
id: randomUUID(),
|
|
2013
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2014
|
-
source: {
|
|
2015
|
-
type: "device",
|
|
2016
|
-
id
|
|
2017
|
-
},
|
|
2018
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2019
|
-
data: {
|
|
2020
|
-
deviceId: id,
|
|
2021
|
-
name: name.length > 0 ? name : stableId,
|
|
2022
|
-
providerId: addonId,
|
|
2023
|
-
parentDeviceId: parentDeviceId ?? null,
|
|
2024
|
-
features: featuresArr
|
|
2025
|
-
}
|
|
2026
|
-
});
|
|
2027
|
-
if (isFirstRegistration || fingerprintChanged) pctx.host.ctx.eventBus.emit({
|
|
2028
|
-
id: randomUUID(),
|
|
2029
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2030
|
-
source: {
|
|
2031
|
-
type: "device",
|
|
2032
|
-
id
|
|
2033
|
-
},
|
|
2034
|
-
category: EventCategory.DeviceProvisioned,
|
|
2035
|
-
data: {
|
|
2036
|
-
deviceId: id,
|
|
2037
|
-
fingerprint: exportFingerprint,
|
|
2038
|
-
generation: Date.now()
|
|
1953
|
+
}
|
|
1954
|
+
return results;
|
|
1955
|
+
}
|
|
1956
|
+
async function getStreamSources(pctx, input) {
|
|
1957
|
+
const { deviceId } = input;
|
|
1958
|
+
if (pctx.registry) {
|
|
1959
|
+
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
1960
|
+
if (found) {
|
|
1961
|
+
if (!isCameraDevice(found.device)) return [];
|
|
1962
|
+
return (await found.device.getStreamSources()).map((s) => ({
|
|
1963
|
+
id: s.id,
|
|
1964
|
+
label: s.label,
|
|
1965
|
+
protocol: s.protocol,
|
|
1966
|
+
url: s.url,
|
|
1967
|
+
resolution: s.resolution,
|
|
1968
|
+
fps: s.fps,
|
|
1969
|
+
bitrate: s.bitrate,
|
|
1970
|
+
codec: s.codec,
|
|
1971
|
+
profileHint: s.profileHint
|
|
1972
|
+
}));
|
|
2039
1973
|
}
|
|
2040
|
-
}
|
|
2041
|
-
if (
|
|
1974
|
+
}
|
|
1975
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
|
|
1976
|
+
return (await pctx.requireDeviceOps(deviceId).getStreamSources({ deviceId })).map((s) => ({ ...s }));
|
|
2042
1977
|
}
|
|
2043
|
-
async function
|
|
1978
|
+
async function getConfigSchema(pctx, input) {
|
|
2044
1979
|
const { deviceId } = input;
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
const bindingKey = String(deviceId);
|
|
2073
|
-
if (bindingsStore.deviceBindings[bindingKey]) {
|
|
2074
|
-
const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
|
|
2075
|
-
await writeBindingsStore(pctx.bindingsDeps, { deviceBindings: restBindings });
|
|
2076
|
-
}
|
|
2077
|
-
pctx.host.remoteNativeCaps.delete(deviceId);
|
|
2078
|
-
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
2079
|
-
pctx.metaStore.idToAddonId.delete(deviceId);
|
|
2080
|
-
pctx.host.devicesWithLinks.delete(deviceId);
|
|
2081
|
-
pctx.host.dropDeviceOverlays(deviceId);
|
|
2082
|
-
await pctx.metaStore.rebuildLinkDependents();
|
|
2083
|
-
pctx.host.ctx.logger.info("removed device", { tags: {
|
|
2084
|
-
deviceId,
|
|
2085
|
-
deviceName: deviceName.length > 0 ? deviceName : stableId
|
|
2086
|
-
} });
|
|
2087
|
-
pctx.host.ctx.eventBus.emit({
|
|
2088
|
-
id: randomUUID(),
|
|
2089
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2090
|
-
source: {
|
|
2091
|
-
type: "device",
|
|
2092
|
-
id: deviceId
|
|
2093
|
-
},
|
|
2094
|
-
category: EventCategory.DeviceUnregistered,
|
|
2095
|
-
data: {
|
|
2096
|
-
deviceId,
|
|
2097
|
-
providerId: addonId,
|
|
2098
|
-
parentDeviceId: persistedMeta.parentDeviceId ?? null
|
|
2099
|
-
}
|
|
2100
|
-
});
|
|
2101
|
-
}
|
|
2102
|
-
async function persistConfig(pctx, input) {
|
|
2103
|
-
const { deviceId, data } = input;
|
|
2104
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] persistConfig: unknown device id=${deviceId}`);
|
|
2105
|
-
await pctx.settings.writeDeviceStore(deviceId, data);
|
|
2106
|
-
}
|
|
2107
|
-
async function loadConfig(pctx, input) {
|
|
2108
|
-
const { deviceId } = input;
|
|
2109
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
2110
|
-
return pctx.settings.readDeviceStore(deviceId);
|
|
2111
|
-
}
|
|
2112
|
-
/**
|
|
2113
|
-
* Load the operator-organisational meta surface for one device
|
|
2114
|
-
* (`name` / `location` / `disabled` / `type` / `parentDeviceId`
|
|
2115
|
-
* / `addonId` + `id` / `stableId`). Used by the kernel proxy's
|
|
2116
|
-
* device-context factory to populate `ctx.deviceMeta` before
|
|
2117
|
-
* the device class constructor runs. Returns `null` when no
|
|
2118
|
-
* persisted row exists for the id.
|
|
2119
|
-
*
|
|
2120
|
-
* Reads default `location` to `null` and `disabled` to `false`
|
|
2121
|
-
* for legacy rows that predate the field — production code
|
|
2122
|
-
* relies on the IDevice type contract that both are present.
|
|
2123
|
-
*/
|
|
2124
|
-
async function loadMeta(pctx, input) {
|
|
2125
|
-
const { deviceId } = input;
|
|
2126
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2127
|
-
if (!persisted) return null;
|
|
2128
|
-
const { addonId, stableId, meta: m } = persisted;
|
|
2129
|
-
const key = String(deviceId);
|
|
2130
|
-
const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
|
|
2131
|
-
return {
|
|
2132
|
-
id: m.id,
|
|
2133
|
-
stableId,
|
|
2134
|
-
addonId,
|
|
2135
|
-
type: m.type,
|
|
2136
|
-
name: m.name,
|
|
2137
|
-
location: m.location ?? null,
|
|
2138
|
-
disabled: m.disabled ?? false,
|
|
2139
|
-
parentDeviceId: m.parentDeviceId,
|
|
2140
|
-
metadata,
|
|
2141
|
-
...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
2142
|
-
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
2143
|
-
...m.role !== void 0 ? { role: m.role } : {}
|
|
2144
|
-
};
|
|
2145
|
-
}
|
|
2146
|
-
/**
|
|
2147
|
-
* Update the operator-edited display name. Writes the meta
|
|
2148
|
-
* row, emits a `DeviceMetaChanged` event so live consumers
|
|
2149
|
-
* (UI device list, alert center) see the rename without
|
|
2150
|
-
* polling. The live `IDevice.name` mirror is updated by the
|
|
2151
|
-
* kernel proxy on its side (`device-cap-proxy.ts`).
|
|
2152
|
-
*/
|
|
2153
|
-
async function setName(pctx, input) {
|
|
2154
|
-
const { deviceId, name } = input;
|
|
2155
|
-
const cascaded = [];
|
|
2156
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2157
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2158
|
-
if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
|
|
2159
|
-
const { meta: m } = persisted;
|
|
2160
|
-
const key = String(deviceId);
|
|
2161
|
-
const oldName = m.name;
|
|
2162
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2163
|
-
const nextMeta = {
|
|
2164
|
-
...allMeta,
|
|
2165
|
-
[key]: {
|
|
2166
|
-
...m,
|
|
2167
|
-
name,
|
|
2168
|
-
userNamed: true
|
|
2169
|
-
}
|
|
2170
|
-
};
|
|
2171
|
-
if (oldName.length > 0 && oldName !== name) for (const [childKey, childMeta] of Object.entries(allMeta)) {
|
|
2172
|
-
if (childKey === key) continue;
|
|
2173
|
-
if (!(childMeta.parentDeviceId === deviceId || childMeta.linkDeviceId === deviceId)) continue;
|
|
2174
|
-
if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
|
|
2175
|
-
const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
|
|
2176
|
-
nextMeta[childKey] = {
|
|
2177
|
-
...childMeta,
|
|
2178
|
-
name: childName
|
|
2179
|
-
};
|
|
2180
|
-
cascaded.push({
|
|
2181
|
-
id: Number(childKey),
|
|
2182
|
-
name: childName
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
await pctx.settings.writeAddonStore({ deviceMeta: nextMeta });
|
|
2186
|
-
});
|
|
2187
|
-
pctx.host.ctx.eventBus.emit({
|
|
2188
|
-
id: randomUUID(),
|
|
2189
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2190
|
-
source: {
|
|
2191
|
-
type: "device",
|
|
2192
|
-
id: deviceId
|
|
2193
|
-
},
|
|
2194
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2195
|
-
data: {
|
|
2196
|
-
deviceId,
|
|
2197
|
-
field: "name",
|
|
2198
|
-
value: name
|
|
2199
|
-
}
|
|
2200
|
-
});
|
|
2201
|
-
for (const child of cascaded) pctx.host.ctx.eventBus.emit({
|
|
2202
|
-
id: randomUUID(),
|
|
2203
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2204
|
-
source: {
|
|
2205
|
-
type: "device",
|
|
2206
|
-
id: child.id
|
|
2207
|
-
},
|
|
2208
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2209
|
-
data: {
|
|
2210
|
-
deviceId: child.id,
|
|
2211
|
-
field: "name",
|
|
2212
|
-
value: child.name
|
|
2213
|
-
}
|
|
2214
|
-
});
|
|
2215
|
-
}
|
|
2216
|
-
/**
|
|
2217
|
-
* Update the operator-organisational location label. `null`
|
|
2218
|
-
* clears it. Mirrors the same persist-then-emit shape as
|
|
2219
|
-
* `setName`; consumers subscribe to `DeviceMetaChanged` and
|
|
2220
|
-
* filter on `field: 'location'`.
|
|
2221
|
-
*/
|
|
2222
|
-
async function setLocation(pctx, input) {
|
|
2223
|
-
const { deviceId, location } = input;
|
|
2224
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2225
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2226
|
-
if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
|
|
2227
|
-
const { meta: m } = persisted;
|
|
2228
|
-
const key = String(deviceId);
|
|
2229
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2230
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2231
|
-
...allMeta,
|
|
2232
|
-
[key]: {
|
|
2233
|
-
...m,
|
|
2234
|
-
location
|
|
2235
|
-
}
|
|
2236
|
-
} });
|
|
2237
|
-
});
|
|
2238
|
-
pctx.host.ctx.eventBus.emit({
|
|
2239
|
-
id: randomUUID(),
|
|
2240
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2241
|
-
source: {
|
|
2242
|
-
type: "device",
|
|
2243
|
-
id: deviceId
|
|
2244
|
-
},
|
|
2245
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2246
|
-
data: {
|
|
2247
|
-
deviceId,
|
|
2248
|
-
field: "location",
|
|
2249
|
-
value: location
|
|
2250
|
-
}
|
|
2251
|
-
});
|
|
2252
|
-
}
|
|
2253
|
-
/**
|
|
2254
|
-
* Update the device type. Writes the meta row, emits a
|
|
2255
|
-
* `DeviceMetaChanged` event. Used by the kernel to apply
|
|
2256
|
-
* `initialMeta.type` before device construction so the device
|
|
2257
|
-
* constructs with its real type instead of the `allocateDeviceId`
|
|
2258
|
-
* placeholder `'generic'`.
|
|
2259
|
-
*/
|
|
2260
|
-
async function setType(pctx, input) {
|
|
2261
|
-
const { deviceId, type } = input;
|
|
2262
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2263
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2264
|
-
if (!persisted) throw new Error(`[device-manager] setType: unknown device id=${deviceId}`);
|
|
2265
|
-
const { meta: m } = persisted;
|
|
2266
|
-
const key = String(deviceId);
|
|
2267
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2268
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2269
|
-
...allMeta,
|
|
2270
|
-
[key]: {
|
|
2271
|
-
...m,
|
|
2272
|
-
type
|
|
2273
|
-
}
|
|
2274
|
-
} });
|
|
2275
|
-
});
|
|
2276
|
-
pctx.host.ctx.eventBus.emit({
|
|
2277
|
-
id: randomUUID(),
|
|
2278
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2279
|
-
source: {
|
|
2280
|
-
type: "device",
|
|
2281
|
-
id: deviceId
|
|
2282
|
-
},
|
|
2283
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2284
|
-
data: {
|
|
2285
|
-
deviceId,
|
|
2286
|
-
field: "type",
|
|
2287
|
-
value: type
|
|
2288
|
-
}
|
|
2289
|
-
});
|
|
2290
|
-
}
|
|
2291
|
-
/**
|
|
2292
|
-
* Stamp (or update) the owning integration id on the device's
|
|
2293
|
-
* meta row. Called by the kernel's `create()` pre-seed path
|
|
2294
|
-
* when `initialMeta.integrationId` is set (analogous to
|
|
2295
|
-
* `setName` / `setType`). Idempotent.
|
|
2296
|
-
*/
|
|
2297
|
-
async function setIntegrationId(pctx, input) {
|
|
2298
|
-
await pctx.stampIntegrationId(input.deviceId, input.integrationId);
|
|
2299
|
-
}
|
|
2300
|
-
/**
|
|
2301
|
-
* Stamp (or update) the linked device id on the device's meta
|
|
2302
|
-
* row. Called by the kernel's `create()` pre-seed path when
|
|
2303
|
-
* `initialMeta.linkDeviceId` is set (analogous to `setIntegrationId`).
|
|
2304
|
-
* Idempotent. `null` explicitly clears a previous link.
|
|
2305
|
-
*/
|
|
2306
|
-
async function setLinkDeviceId(pctx, input) {
|
|
2307
|
-
const { deviceId, linkDeviceId } = input;
|
|
2308
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2309
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2310
|
-
if (!persisted) throw new Error(`[device-manager] setLinkDeviceId: unknown device id=${deviceId}`);
|
|
2311
|
-
const { meta: m } = persisted;
|
|
2312
|
-
const key = String(deviceId);
|
|
2313
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2314
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2315
|
-
...allMeta,
|
|
2316
|
-
[key]: {
|
|
2317
|
-
...m,
|
|
2318
|
-
linkDeviceId
|
|
2319
|
-
}
|
|
2320
|
-
} });
|
|
2321
|
-
});
|
|
2322
|
-
pctx.host.ctx.eventBus.emit({
|
|
2323
|
-
id: randomUUID(),
|
|
2324
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2325
|
-
source: {
|
|
2326
|
-
type: "device",
|
|
2327
|
-
id: deviceId
|
|
2328
|
-
},
|
|
2329
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2330
|
-
data: {
|
|
2331
|
-
deviceId,
|
|
2332
|
-
field: "linkDeviceId",
|
|
2333
|
-
value: linkDeviceId
|
|
2334
|
-
}
|
|
2335
|
-
});
|
|
2336
|
-
}
|
|
2337
|
-
/**
|
|
2338
|
-
* Set (or clear) the durable primary-child override on a CONTAINER
|
|
2339
|
-
* device's meta row, keyed on the chosen child's re-sync/rename-stable
|
|
2340
|
-
* `entityId`. Mirrors `setLinkDeviceId` but persists a string entityId
|
|
2341
|
-
* (survives a re-sync that reallocates the child's numeric id) instead
|
|
2342
|
-
* of the numeric child id. `null` clears the override → priority default.
|
|
2343
|
-
*/
|
|
2344
|
-
async function setPrimaryChildEntityId(pctx, input) {
|
|
2345
|
-
const { deviceId, primaryChildEntityId } = input;
|
|
2346
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2347
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2348
|
-
if (!persisted) throw new Error(`[device-manager] setPrimaryChildEntityId: unknown device id=${deviceId}`);
|
|
2349
|
-
const { meta: m } = persisted;
|
|
2350
|
-
const key = String(deviceId);
|
|
2351
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2352
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2353
|
-
...allMeta,
|
|
2354
|
-
[key]: {
|
|
2355
|
-
...m,
|
|
2356
|
-
primaryChildEntityId
|
|
2357
|
-
}
|
|
2358
|
-
} });
|
|
2359
|
-
});
|
|
2360
|
-
pctx.host.ctx.eventBus.emit({
|
|
2361
|
-
id: randomUUID(),
|
|
2362
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2363
|
-
source: {
|
|
2364
|
-
type: "device",
|
|
2365
|
-
id: deviceId
|
|
2366
|
-
},
|
|
2367
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2368
|
-
data: {
|
|
2369
|
-
deviceId,
|
|
2370
|
-
field: "primaryChildEntityId",
|
|
2371
|
-
value: primaryChildEntityId
|
|
2372
|
-
}
|
|
2373
|
-
});
|
|
2374
|
-
}
|
|
2375
|
-
/**
|
|
2376
|
-
* Set (or replace) the container-level `childLayout` on a device's
|
|
2377
|
-
* meta row. Mirrors `setPrimaryChildEntityId` but persists the
|
|
2378
|
-
* accordion-section layout array (parent-only). Persisted, projected,
|
|
2379
|
-
* and preserved across re-register/restore.
|
|
2380
|
-
*/
|
|
2381
|
-
async function setChildLayout(pctx, input) {
|
|
2382
|
-
const { deviceId, childLayout } = input;
|
|
2383
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2384
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2385
|
-
if (!persisted) throw new Error(`[device-manager] setChildLayout: unknown device id=${deviceId}`);
|
|
2386
|
-
const { meta: m } = persisted;
|
|
2387
|
-
const key = String(deviceId);
|
|
2388
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2389
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2390
|
-
...allMeta,
|
|
2391
|
-
[key]: {
|
|
2392
|
-
...m,
|
|
2393
|
-
childLayout
|
|
2394
|
-
}
|
|
2395
|
-
} });
|
|
2396
|
-
});
|
|
2397
|
-
pctx.host.ctx.eventBus.emit({
|
|
2398
|
-
id: randomUUID(),
|
|
2399
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2400
|
-
source: {
|
|
2401
|
-
type: "device",
|
|
2402
|
-
id: deviceId
|
|
2403
|
-
},
|
|
2404
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2405
|
-
data: {
|
|
2406
|
-
deviceId,
|
|
2407
|
-
field: "childLayout",
|
|
2408
|
-
value: childLayout
|
|
2409
|
-
}
|
|
2410
|
-
});
|
|
2411
|
-
}
|
|
2412
|
-
/**
|
|
2413
|
-
* Set (or replace) the operator-authored cross-device field wirings on
|
|
2414
|
-
* a device's meta row. Mirrors `setChildLayout` but persists the
|
|
2415
|
-
* `deviceLinks` array. Persisted, projected, and preserved across
|
|
2416
|
-
* re-register/restore. Also maintains the in-memory `devicesWithLinks`
|
|
2417
|
-
* Set used as an O(1) gate by `resolveLinkedStatus`.
|
|
2418
|
-
*/
|
|
2419
|
-
async function setDeviceLinks(pctx, input) {
|
|
2420
|
-
const { deviceId, deviceLinks } = input;
|
|
2421
|
-
const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
|
|
2422
|
-
if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
|
|
2423
|
-
for (const link of deviceLinks) {
|
|
2424
|
-
if (link.source.kind !== "expression") continue;
|
|
2425
|
-
const err = validateExpressionSource(link.source);
|
|
2426
|
-
if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
|
|
2427
|
-
}
|
|
2428
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2429
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2430
|
-
if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
|
|
2431
|
-
const { meta: m } = persisted;
|
|
2432
|
-
const key = String(deviceId);
|
|
2433
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2434
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2435
|
-
...allMeta,
|
|
2436
|
-
[key]: {
|
|
2437
|
-
...m,
|
|
2438
|
-
deviceLinks
|
|
2439
|
-
}
|
|
2440
|
-
} });
|
|
2441
|
-
});
|
|
2442
|
-
if (deviceLinks.length > 0) pctx.host.devicesWithLinks.add(deviceId);
|
|
2443
|
-
else pctx.host.devicesWithLinks.delete(deviceId);
|
|
2444
|
-
await pctx.metaStore.rebuildLinkDependents();
|
|
2445
|
-
pctx.host.ctx.eventBus.emit({
|
|
2446
|
-
id: randomUUID(),
|
|
2447
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2448
|
-
source: {
|
|
2449
|
-
type: "device",
|
|
2450
|
-
id: deviceId
|
|
2451
|
-
},
|
|
2452
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2453
|
-
data: {
|
|
2454
|
-
deviceId,
|
|
2455
|
-
field: "deviceLinks",
|
|
2456
|
-
value: deviceLinks
|
|
2457
|
-
}
|
|
2458
|
-
});
|
|
2459
|
-
}
|
|
2460
|
-
/**
|
|
2461
|
-
* Stamp (or update) the semantic role on the device's meta row.
|
|
2462
|
-
* Called by the kernel's `create()` / `spawnAccessoryChild`
|
|
2463
|
-
* pre-seed when `initialMeta.role` is set (analogous to
|
|
2464
|
-
* `setIntegrationId`). Idempotent. `null` clears a previous role.
|
|
2465
|
-
*/
|
|
2466
|
-
async function setRole(pctx, input) {
|
|
2467
|
-
const { deviceId, role } = input;
|
|
2468
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2469
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2470
|
-
if (!persisted) throw new Error(`[device-manager] setRole: unknown device id=${deviceId}`);
|
|
2471
|
-
const { meta: m } = persisted;
|
|
2472
|
-
const key = String(deviceId);
|
|
2473
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2474
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2475
|
-
...allMeta,
|
|
2476
|
-
[key]: {
|
|
2477
|
-
...m,
|
|
2478
|
-
role
|
|
2479
|
-
}
|
|
2480
|
-
} });
|
|
2481
|
-
});
|
|
2482
|
-
pctx.host.ctx.eventBus.emit({
|
|
2483
|
-
id: randomUUID(),
|
|
2484
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2485
|
-
source: {
|
|
2486
|
-
type: "device",
|
|
2487
|
-
id: deviceId
|
|
2488
|
-
},
|
|
2489
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2490
|
-
data: {
|
|
2491
|
-
deviceId,
|
|
2492
|
-
field: "role",
|
|
2493
|
-
value: role
|
|
2494
|
-
}
|
|
2495
|
-
});
|
|
2496
|
-
}
|
|
2497
|
-
/**
|
|
2498
|
-
* Normalize an operator-authored display override at write time so the render
|
|
2499
|
-
* path's `UNIT_TABLE` lookups always hit canonical spellings — maps `unit` and
|
|
2500
|
-
* every `perCap[*].unit` through `normalizeUnit`. Pure: rebuilds new objects,
|
|
2501
|
-
* never mutates the input. A unit `normalizeUnit` cannot canonicalize is left
|
|
2502
|
-
* as-is (the render path refuses conversion for unknown spellings).
|
|
2503
|
-
*/
|
|
2504
|
-
function normalizeDisplayOverride(display) {
|
|
2505
|
-
return {
|
|
2506
|
-
...display,
|
|
2507
|
-
...display.unit !== void 0 ? { unit: normalizeUnit(display.unit) ?? display.unit } : {},
|
|
2508
|
-
...display.perCap !== void 0 ? { perCap: Object.fromEntries(Object.entries(display.perCap).map(([cap, refine]) => [cap, refine.unit !== void 0 ? {
|
|
2509
|
-
...refine,
|
|
2510
|
-
unit: normalizeUnit(refine.unit) ?? refine.unit
|
|
2511
|
-
} : refine])) } : {}
|
|
2512
|
-
};
|
|
2513
|
-
}
|
|
2514
|
-
/**
|
|
2515
|
-
* Set (or clear) the per-device display override on a device's meta row.
|
|
2516
|
-
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
2517
|
-
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
2518
|
-
* projection contract, never persists `display: undefined`). Override units are
|
|
2519
|
-
* normalized at write so the render path always looks up canonical spellings.
|
|
2520
|
-
*/
|
|
2521
|
-
async function setDisplay(pctx, input) {
|
|
2522
|
-
const { deviceId, display } = input;
|
|
2523
|
-
const normalized = display === null ? null : normalizeDisplayOverride(display);
|
|
2524
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2525
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2526
|
-
if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
|
|
2527
|
-
const { meta: m } = persisted;
|
|
2528
|
-
const key = String(deviceId);
|
|
2529
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2530
|
-
const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
|
|
2531
|
-
...m,
|
|
2532
|
-
display: normalized
|
|
2533
|
-
};
|
|
2534
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2535
|
-
...allMeta,
|
|
2536
|
-
[key]: nextRow
|
|
2537
|
-
} });
|
|
2538
|
-
});
|
|
2539
|
-
pctx.host.ctx.eventBus.emit({
|
|
2540
|
-
id: randomUUID(),
|
|
2541
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2542
|
-
source: {
|
|
2543
|
-
type: "device",
|
|
2544
|
-
id: deviceId
|
|
2545
|
-
},
|
|
2546
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2547
|
-
data: {
|
|
2548
|
-
deviceId,
|
|
2549
|
-
field: "display",
|
|
2550
|
-
value: normalized
|
|
2551
|
-
}
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
/**
|
|
2555
|
-
* Read the operator-authored per-role display defaults. Empty record when none
|
|
2556
|
-
* set. Not per-device — a plain top-level-key read.
|
|
2557
|
-
*/
|
|
2558
|
-
async function getRoleDisplayDefaults(pctx, _input) {
|
|
2559
|
-
return { defaults: (await pctx.metaStore.readStore()).roleDisplayDefaults ?? {} };
|
|
2560
|
-
}
|
|
2561
|
-
/**
|
|
2562
|
-
* Replace the per-role display defaults whole-record (full replace). Override
|
|
2563
|
-
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
2564
|
-
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
2565
|
-
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
2566
|
-
* is emitted; the UI invalidates its own query on mutate.
|
|
2567
|
-
*/
|
|
2568
|
-
async function setRoleDisplayDefaults(pctx, input) {
|
|
2569
|
-
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
2570
|
-
...def,
|
|
2571
|
-
unit: normalizeUnit(def.unit) ?? def.unit
|
|
2572
|
-
} : def]));
|
|
2573
|
-
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
2574
|
-
}
|
|
2575
|
-
/**
|
|
2576
|
-
* Batched meta pre-seed. Applies every provided field to the
|
|
2577
|
-
* device's meta row in ONE read-modify-write under a single
|
|
2578
|
-
* `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
|
|
2579
|
-
* then emits one `DeviceMetaChanged` event per field that was
|
|
2580
|
-
* supplied — preserving the exact semantics of the individual
|
|
2581
|
-
* setters (`setName` / `setLocation` / `setType` /
|
|
2582
|
-
* `setIntegrationId` / `setLinkDeviceId` / `setRole`). Omitted
|
|
2583
|
-
* fields are left untouched; `null` clears `location` /
|
|
2584
|
-
* `linkDeviceId` / `role`. Idempotent.
|
|
2585
|
-
*
|
|
2586
|
-
* Collapses the per-child meta pre-seed (up to 6 individual
|
|
2587
|
-
* setter round-trips, each its own lock + write) into one — the
|
|
2588
|
-
* dominant cost when the kernel's `spawnAccessoryChild` reconciles
|
|
2589
|
-
* a many-entity container.
|
|
2590
|
-
*/
|
|
2591
|
-
async function applyInitialMeta(pctx, input) {
|
|
2592
|
-
const { deviceId, name, location, type, integrationId, linkDeviceId, role } = input;
|
|
2593
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2594
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2595
|
-
if (!persisted) throw new Error(`[device-manager] applyInitialMeta: unknown device id=${deviceId}`);
|
|
2596
|
-
const { meta: m } = persisted;
|
|
2597
|
-
const key = String(deviceId);
|
|
2598
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2599
|
-
const merged = {
|
|
2600
|
-
...m,
|
|
2601
|
-
...name !== void 0 ? { name } : {},
|
|
2602
|
-
...location !== void 0 ? { location } : {},
|
|
2603
|
-
...type !== void 0 ? { type } : {},
|
|
2604
|
-
...integrationId !== void 0 ? { integrationId } : {},
|
|
2605
|
-
...linkDeviceId !== void 0 ? { linkDeviceId } : {},
|
|
2606
|
-
...role !== void 0 ? { role } : {}
|
|
2607
|
-
};
|
|
2608
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2609
|
-
...allMeta,
|
|
2610
|
-
[key]: merged
|
|
2611
|
-
} });
|
|
2612
|
-
});
|
|
2613
|
-
const emitMetaChanged = (field, value) => {
|
|
2614
|
-
pctx.host.ctx.eventBus.emit({
|
|
2615
|
-
id: randomUUID(),
|
|
2616
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2617
|
-
source: {
|
|
2618
|
-
type: "device",
|
|
2619
|
-
id: deviceId
|
|
2620
|
-
},
|
|
2621
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2622
|
-
data: {
|
|
2623
|
-
deviceId,
|
|
2624
|
-
field,
|
|
2625
|
-
value
|
|
2626
|
-
}
|
|
2627
|
-
});
|
|
2628
|
-
};
|
|
2629
|
-
if (name !== void 0) emitMetaChanged("name", name);
|
|
2630
|
-
if (location !== void 0) emitMetaChanged("location", location);
|
|
2631
|
-
if (type !== void 0) emitMetaChanged("type", type);
|
|
2632
|
-
if (integrationId !== void 0) emitMetaChanged("integrationId", integrationId);
|
|
2633
|
-
if (linkDeviceId !== void 0) emitMetaChanged("linkDeviceId", linkDeviceId);
|
|
2634
|
-
if (role !== void 0) emitMetaChanged("role", role);
|
|
2635
|
-
}
|
|
2636
|
-
/**
|
|
2637
|
-
* Patch the device's hardware-identity metadata blob. Shallow
|
|
2638
|
-
* merge — `null` removes a key, anything else overwrites.
|
|
2639
|
-
* Drivers populate factual fields on first probe; operators
|
|
2640
|
-
* augment via the Device Info tab. Idempotent: a no-op patch
|
|
2641
|
-
* (every key already present with the same value) doesn't emit
|
|
2642
|
-
* the meta-changed event.
|
|
2643
|
-
*/
|
|
2644
|
-
async function setMetadata(pctx, input) {
|
|
2645
|
-
const { deviceId, patch } = input;
|
|
2646
|
-
const result = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2647
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
|
|
2648
|
-
const key = String(deviceId);
|
|
2649
|
-
const map = await pctx.metaStore.readMetadataMap();
|
|
2650
|
-
const next = { ...map[key] ?? {} };
|
|
2651
|
-
let changed = false;
|
|
2652
|
-
for (const [k, v] of Object.entries(patch)) if (v === null) {
|
|
2653
|
-
if (k in next) {
|
|
2654
|
-
delete next[k];
|
|
2655
|
-
changed = true;
|
|
2656
|
-
}
|
|
2657
|
-
} else if (next[k] !== v) {
|
|
2658
|
-
next[k] = v;
|
|
2659
|
-
changed = true;
|
|
2660
|
-
}
|
|
2661
|
-
if (!changed) return { changed: false };
|
|
2662
|
-
const hasFields = Object.keys(next).length > 0;
|
|
2663
|
-
const updatedMap = { ...map };
|
|
2664
|
-
if (hasFields) updatedMap[key] = next;
|
|
2665
|
-
else delete updatedMap[key];
|
|
2666
|
-
await pctx.settings.writeAddonStore({ deviceMetadata: updatedMap });
|
|
2667
|
-
return {
|
|
2668
|
-
changed: true,
|
|
2669
|
-
finalMeta: hasFields ? next : null
|
|
2670
|
-
};
|
|
2671
|
-
});
|
|
2672
|
-
if (!result.changed) return;
|
|
2673
|
-
pctx.host.ctx.eventBus.emit({
|
|
2674
|
-
id: randomUUID(),
|
|
2675
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2676
|
-
source: {
|
|
2677
|
-
type: "device",
|
|
2678
|
-
id: deviceId
|
|
2679
|
-
},
|
|
2680
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2681
|
-
data: {
|
|
2682
|
-
deviceId,
|
|
2683
|
-
field: "metadata",
|
|
2684
|
-
value: result.finalMeta
|
|
1980
|
+
if (pctx.registry) {
|
|
1981
|
+
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
1982
|
+
if (found) return found.device.config.entries().map((entry) => ({
|
|
1983
|
+
key: entry.key,
|
|
1984
|
+
value: entry.value,
|
|
1985
|
+
...entry.description !== void 0 ? { description: entry.description } : {}
|
|
1986
|
+
}));
|
|
1987
|
+
}
|
|
1988
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
|
|
1989
|
+
return (await pctx.requireDeviceOps(deviceId).getConfigEntries({ deviceId })).map((e) => ({ ...e }));
|
|
1990
|
+
}
|
|
1991
|
+
async function getSettingsSchema(pctx, input) {
|
|
1992
|
+
const { deviceId } = input;
|
|
1993
|
+
if (pctx.registry) {
|
|
1994
|
+
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
1995
|
+
if (found) return found.device.getSettingsUISchema();
|
|
1996
|
+
}
|
|
1997
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return null;
|
|
1998
|
+
return await pctx.requireDeviceOps(deviceId).getSettingsSchema({ deviceId }) ?? null;
|
|
1999
|
+
}
|
|
2000
|
+
async function updateConfig(pctx, input) {
|
|
2001
|
+
const { deviceId } = input;
|
|
2002
|
+
if (pctx.registry) {
|
|
2003
|
+
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
2004
|
+
if (found) {
|
|
2005
|
+
await found.device.config.setAll(input.values);
|
|
2006
|
+
return { success: true };
|
|
2685
2007
|
}
|
|
2008
|
+
}
|
|
2009
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
|
|
2010
|
+
await pctx.requireDeviceOps(deviceId).setConfig({
|
|
2011
|
+
deviceId,
|
|
2012
|
+
values: input.values
|
|
2686
2013
|
});
|
|
2014
|
+
return { success: true };
|
|
2687
2015
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
* of its lifecycle methods).
|
|
2693
|
-
*/
|
|
2694
|
-
async function setDisabled(pctx, input) {
|
|
2695
|
-
const { deviceId, disabled } = input;
|
|
2696
|
-
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2697
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2698
|
-
if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
|
|
2699
|
-
const { meta: m } = persisted;
|
|
2700
|
-
const key = String(deviceId);
|
|
2701
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
2702
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2703
|
-
...allMeta,
|
|
2704
|
-
[key]: {
|
|
2705
|
-
...m,
|
|
2706
|
-
disabled
|
|
2707
|
-
}
|
|
2708
|
-
} });
|
|
2016
|
+
async function enable(pctx, input) {
|
|
2017
|
+
await pctx.provider.setDisabled({
|
|
2018
|
+
deviceId: input.deviceId,
|
|
2019
|
+
disabled: false
|
|
2709
2020
|
});
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
},
|
|
2717
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2718
|
-
data: {
|
|
2719
|
-
deviceId,
|
|
2720
|
-
field: "disabled",
|
|
2721
|
-
value: disabled
|
|
2722
|
-
}
|
|
2021
|
+
return { success: true };
|
|
2022
|
+
}
|
|
2023
|
+
async function disable(pctx, input) {
|
|
2024
|
+
await pctx.provider.setDisabled({
|
|
2025
|
+
deviceId: input.deviceId,
|
|
2026
|
+
disabled: true
|
|
2723
2027
|
});
|
|
2028
|
+
return { success: true };
|
|
2724
2029
|
}
|
|
2725
|
-
async function
|
|
2030
|
+
async function remove(pctx, input) {
|
|
2726
2031
|
const { deviceId } = input;
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2032
|
+
const removeOne = async (id) => {
|
|
2033
|
+
if (pctx.registry) {
|
|
2034
|
+
const live = resolveDeviceById(pctx.registry, id);
|
|
2035
|
+
if (live) {
|
|
2036
|
+
const deviceName = live.device.name;
|
|
2037
|
+
await live.device.removeDevice();
|
|
2038
|
+
pctx.registry.remove(id);
|
|
2039
|
+
await pctx.provider.removeDevice({ deviceId: id });
|
|
2040
|
+
pctx.host.ctx.logger.info("removed hub-local device", { tags: {
|
|
2041
|
+
deviceId: id,
|
|
2042
|
+
deviceName
|
|
2043
|
+
} });
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const persisted = await pctx.metaStore.resolvePersistedById(id);
|
|
2048
|
+
if (!persisted) return;
|
|
2049
|
+
const { meta: persistedMeta } = persisted;
|
|
2050
|
+
try {
|
|
2051
|
+
await pctx.requireDeviceOps(id).removeDevice({ deviceId: id });
|
|
2052
|
+
} catch (err) {
|
|
2053
|
+
pctx.host.ctx.logger.warn("remove via device-ops failed — clearing persistence anyway", {
|
|
2054
|
+
tags: {
|
|
2055
|
+
deviceId: id,
|
|
2056
|
+
deviceName: persistedMeta.name
|
|
2057
|
+
},
|
|
2058
|
+
meta: { error: errMsg(err) }
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
await pctx.provider.removeDevice({ deviceId: id });
|
|
2750
2062
|
};
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
* existing entries (case-insensitive match) are silently kept.
|
|
2758
|
-
* Empty / whitespace-only inputs throw — operators must supply a
|
|
2759
|
-
* meaningful label.
|
|
2760
|
-
*/
|
|
2761
|
-
async function addLocation(pctx, input) {
|
|
2762
|
-
const trimmed = input.name.trim();
|
|
2763
|
-
if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
|
|
2764
|
-
const current = (await pctx.settings.readAddonStore()).locations ?? [];
|
|
2765
|
-
if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
|
|
2766
|
-
await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
|
|
2063
|
+
const removeCascade = async (id) => {
|
|
2064
|
+
for (const childId of await pctx.metaStore.directChildIds(id)) await removeCascade(childId);
|
|
2065
|
+
await removeOne(id);
|
|
2066
|
+
};
|
|
2067
|
+
await removeCascade(deviceId);
|
|
2068
|
+
return { success: true };
|
|
2767
2069
|
}
|
|
2768
2070
|
/**
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
*
|
|
2772
|
-
*
|
|
2773
|
-
* `
|
|
2774
|
-
*
|
|
2775
|
-
* as the registry equality check.
|
|
2071
|
+
* Cascade-delete every top-level device whose `integrationId`
|
|
2072
|
+
* matches. Enumerates a SNAPSHOT of the meta map so concurrent
|
|
2073
|
+
* removals don't clobber each other. Only top-level parents are
|
|
2074
|
+
* enumerated — children cascade via the per-parent `removeCascade`
|
|
2075
|
+
* inside the delegated `remove` call. Idempotent: devices with no
|
|
2076
|
+
* `integrationId` never match.
|
|
2776
2077
|
*/
|
|
2777
|
-
async function
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2780
|
-
const
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
if (remaining.length !== current.length) await pctx.settings.writeAddonStore({ locations: remaining });
|
|
2784
|
-
if (input.cascade !== true) return;
|
|
2785
|
-
const meta = store.deviceMeta ?? {};
|
|
2786
|
-
const updates = { ...meta };
|
|
2787
|
-
const cleared = [];
|
|
2788
|
-
for (const [key, m] of Object.entries(meta)) {
|
|
2789
|
-
if (typeof m.location !== "string") continue;
|
|
2790
|
-
if (m.location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
|
|
2791
|
-
updates[key] = {
|
|
2792
|
-
...m,
|
|
2793
|
-
location: null
|
|
2794
|
-
};
|
|
2795
|
-
cleared.push(m.id);
|
|
2796
|
-
}
|
|
2797
|
-
if (cleared.length === 0) return;
|
|
2798
|
-
await pctx.settings.writeAddonStore({ deviceMeta: updates });
|
|
2799
|
-
for (const deviceId of cleared) pctx.host.ctx.eventBus.emit({
|
|
2800
|
-
id: randomUUID(),
|
|
2801
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2802
|
-
source: {
|
|
2803
|
-
type: "device",
|
|
2804
|
-
id: deviceId
|
|
2805
|
-
},
|
|
2806
|
-
category: EventCategory.DeviceMetaChanged,
|
|
2807
|
-
data: {
|
|
2808
|
-
deviceId,
|
|
2809
|
-
field: "location",
|
|
2810
|
-
value: null
|
|
2811
|
-
}
|
|
2078
|
+
async function removeByIntegration(pctx, input) {
|
|
2079
|
+
const { integrationId } = input;
|
|
2080
|
+
const meta = await pctx.metaStore.readMeta();
|
|
2081
|
+
const parentKeys = Object.keys(meta).filter((key) => {
|
|
2082
|
+
const m = meta[key];
|
|
2083
|
+
return m !== void 0 && m.integrationId === integrationId && m.parentDeviceId === null;
|
|
2812
2084
|
});
|
|
2085
|
+
let removed = 0;
|
|
2086
|
+
for (const _key of parentKeys) {
|
|
2087
|
+
const m = meta[_key];
|
|
2088
|
+
if (!m) continue;
|
|
2089
|
+
await pctx.provider.remove({ deviceId: m.id });
|
|
2090
|
+
removed++;
|
|
2091
|
+
}
|
|
2092
|
+
return { removed };
|
|
2813
2093
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2094
|
+
async function getStreamProfileMap(pctx, input) {
|
|
2095
|
+
if (!pctx.registry) return {};
|
|
2096
|
+
const found = resolveDeviceById(pctx.registry, input.deviceId);
|
|
2097
|
+
if (!found) return {};
|
|
2098
|
+
const storedMap = found.device.config.entries().find((e) => e.key === "_profileMap")?.value;
|
|
2099
|
+
if (storedMap !== void 0 && typeof storedMap === "object" && storedMap !== null) return storedMap;
|
|
2100
|
+
if (!isCameraDevice(found.device)) return {};
|
|
2101
|
+
const sources = await found.device.getStreamSources();
|
|
2102
|
+
const profileMap = {};
|
|
2103
|
+
for (const s of sources) if (s.profileHint && s.id) profileMap[s.profileHint] = s.id;
|
|
2104
|
+
return profileMap;
|
|
2105
|
+
}
|
|
2106
|
+
async function setStreamProfileMap(pctx, input) {
|
|
2107
|
+
const { deviceId } = input;
|
|
2108
|
+
if (pctx.registry) {
|
|
2109
|
+
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
2110
|
+
if (found) {
|
|
2111
|
+
await found.device.config.setAll({ _profileMap: input.profileMap });
|
|
2112
|
+
return { success: true };
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
|
|
2116
|
+
await pctx.requireDeviceOps(deviceId).setConfig({
|
|
2117
|
+
deviceId,
|
|
2118
|
+
values: { _profileMap: input.profileMap }
|
|
2119
|
+
});
|
|
2120
|
+
return { success: true };
|
|
2121
|
+
}
|
|
2122
|
+
async function probeStreams(pctx, input) {
|
|
2123
|
+
const streamProbe = pctx.host.ctx.kernel.streamProbe;
|
|
2124
|
+
if (!streamProbe) return [];
|
|
2125
|
+
const sources = await pctx.provider.getStreamSources({ deviceId: input.deviceId });
|
|
2126
|
+
const results = [];
|
|
2127
|
+
for (const s of sources) {
|
|
2128
|
+
if (!s.url) continue;
|
|
2129
|
+
try {
|
|
2130
|
+
const metadata = await streamProbe.probe(s.url, { force: true });
|
|
2131
|
+
results.push({
|
|
2132
|
+
streamId: s.id,
|
|
2133
|
+
width: metadata.width,
|
|
2134
|
+
height: metadata.height,
|
|
2135
|
+
codec: metadata.codec,
|
|
2136
|
+
fps: metadata.fps,
|
|
2137
|
+
bitrateKbps: metadata.bitrateKbps
|
|
2835
2138
|
});
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
}
|
|
2844
|
-
continue;
|
|
2139
|
+
} catch (err) {
|
|
2140
|
+
pctx.host.ctx.logger.debug("streamProbe.probe failed — returning placeholder", { meta: {
|
|
2141
|
+
deviceId: input.deviceId,
|
|
2142
|
+
streamId: s.id,
|
|
2143
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2144
|
+
} });
|
|
2145
|
+
results.push({ streamId: s.id });
|
|
2845
2146
|
}
|
|
2846
|
-
if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
|
|
2847
2147
|
}
|
|
2848
|
-
return
|
|
2849
|
-
targets,
|
|
2850
|
-
dependents
|
|
2851
|
-
};
|
|
2148
|
+
return results;
|
|
2852
2149
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
linkHost;
|
|
2878
|
-
/** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
|
|
2879
|
-
* The persisted meta store is authoritative but reads are async; hub-side
|
|
2880
|
-
* callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
|
|
2881
|
-
* ownership without awaiting. Kept in sync with every register/remove and
|
|
2882
|
-
* warmed from persistence on boot. */
|
|
2883
|
-
idToAddonId = /* @__PURE__ */ new Map();
|
|
2884
|
-
/** Serialises every read-modify-write of the deviceMeta / deviceIndex blob
|
|
2885
|
-
* through one promise chain (see `withMetaWriteLock`). Per-instance state —
|
|
2886
|
-
* identical to the former `onInitialize` closure variable. */
|
|
2887
|
-
metaWriteChain = Promise.resolve();
|
|
2888
|
-
constructor(settings, registry, linkHost) {
|
|
2889
|
-
this.settings = settings;
|
|
2890
|
-
this.registry = registry;
|
|
2891
|
-
this.linkHost = linkHost;
|
|
2150
|
+
async function discoverDevices(pctx, input) {
|
|
2151
|
+
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
2152
|
+
if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device discovery`);
|
|
2153
|
+
return (await dp.discoverDevices({})).map((d) => ({
|
|
2154
|
+
stableId: d.stableId,
|
|
2155
|
+
type: d.type,
|
|
2156
|
+
suggestedName: d.suggestedName,
|
|
2157
|
+
prefilledConfig: d.prefilledConfig
|
|
2158
|
+
}));
|
|
2159
|
+
}
|
|
2160
|
+
async function adoptDevice(pctx, input) {
|
|
2161
|
+
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
2162
|
+
if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device adoption`);
|
|
2163
|
+
const summary = await dp.adoptDiscoveredDevice({ candidate: input.candidate });
|
|
2164
|
+
if (input.integrationId !== void 0) try {
|
|
2165
|
+
await pctx.stampIntegrationId(summary.id, input.integrationId);
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
pctx.host.ctx.logger.warn("adoptDevice: integrationId stamp failed (device adopted)", {
|
|
2168
|
+
tags: {
|
|
2169
|
+
deviceId: summary.id,
|
|
2170
|
+
integrationId: input.integrationId
|
|
2171
|
+
},
|
|
2172
|
+
meta: { error: errMsg(err) }
|
|
2173
|
+
});
|
|
2892
2174
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
};
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2175
|
+
return summary;
|
|
2176
|
+
}
|
|
2177
|
+
async function getCreationSchema(pctx, input) {
|
|
2178
|
+
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
2179
|
+
if (!await dp.supportsManualCreation({})) return null;
|
|
2180
|
+
return await dp.getChildCreationSchema({ type: input.type }) ?? null;
|
|
2181
|
+
}
|
|
2182
|
+
async function createDevice(pctx, input) {
|
|
2183
|
+
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
2184
|
+
if (!await dp.supportsManualCreation({})) throw new Error(`Addon "${input.addonId}" does not support manual device creation`);
|
|
2185
|
+
const summary = await dp.createDevice({
|
|
2186
|
+
type: input.type,
|
|
2187
|
+
config: input.config
|
|
2188
|
+
});
|
|
2189
|
+
if (input.integrationId !== void 0) try {
|
|
2190
|
+
await pctx.stampIntegrationId(summary.id, input.integrationId);
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
pctx.host.ctx.logger.warn("createDevice: integrationId stamp failed (device created)", {
|
|
2193
|
+
tags: {
|
|
2194
|
+
deviceId: summary.id,
|
|
2195
|
+
integrationId: input.integrationId
|
|
2196
|
+
},
|
|
2197
|
+
meta: { error: errMsg(err) }
|
|
2916
2198
|
});
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2199
|
+
}
|
|
2200
|
+
return summary;
|
|
2201
|
+
}
|
|
2202
|
+
async function testCreationField(pctx, input) {
|
|
2203
|
+
return (await pctx.host.requireDeviceProvider(input.addonId)).testCreationField({
|
|
2204
|
+
type: input.type,
|
|
2205
|
+
key: input.key,
|
|
2206
|
+
value: input.value,
|
|
2207
|
+
...input.formValues !== void 0 ? { formValues: input.formValues } : {}
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
async function adoptionListCandidates(pctx, input) {
|
|
2211
|
+
const { addonId, ...rest } = input;
|
|
2212
|
+
return (await pctx.host.requireDeviceAdoptionProvider(addonId)).listCandidates(rest);
|
|
2213
|
+
}
|
|
2214
|
+
async function adoptionRefresh(pctx, input) {
|
|
2215
|
+
const { addonId, integrationId } = input;
|
|
2216
|
+
return (await pctx.host.requireDeviceAdoptionProvider(addonId)).refresh({ integrationId });
|
|
2217
|
+
}
|
|
2218
|
+
async function adoptionAdopt(pctx, input) {
|
|
2219
|
+
const { addonId, ...rest } = input;
|
|
2220
|
+
return (await pctx.host.requireDeviceAdoptionProvider(addonId)).adopt(rest);
|
|
2221
|
+
}
|
|
2222
|
+
async function adoptionRelease(pctx, input) {
|
|
2223
|
+
const { addonId, ...rest } = input;
|
|
2224
|
+
return (await pctx.host.requireDeviceAdoptionProvider(addonId)).release(rest);
|
|
2225
|
+
}
|
|
2226
|
+
async function adoptionResync(pctx, input) {
|
|
2227
|
+
const { camDeviceId, resetToSource } = input;
|
|
2228
|
+
let owningAddonId = pctx.registry?.getAddonId(camDeviceId) ?? null;
|
|
2229
|
+
if (!owningAddonId) owningAddonId = (await pctx.metaStore.resolvePersistedById(camDeviceId))?.addonId ?? null;
|
|
2230
|
+
if (!owningAddonId) throw new Error(`adoptionResync: device ${camDeviceId} not found`);
|
|
2231
|
+
let removedChildren = 0;
|
|
2232
|
+
if (resetToSource === true) {
|
|
2233
|
+
const childIds = await pctx.metaStore.directChildIds(camDeviceId);
|
|
2234
|
+
for (const childId of childIds) {
|
|
2235
|
+
await pctx.provider.remove({ deviceId: childId });
|
|
2236
|
+
removedChildren += 1;
|
|
2923
2237
|
}
|
|
2238
|
+
pctx.host.ctx.logger.info("resetToSource purge before resync", { tags: {
|
|
2239
|
+
deviceId: camDeviceId,
|
|
2240
|
+
removedChildren
|
|
2241
|
+
} });
|
|
2242
|
+
}
|
|
2243
|
+
return {
|
|
2244
|
+
...await (await pctx.host.requireDeviceAdoptionProvider(owningAddonId)).resync({
|
|
2245
|
+
camDeviceId,
|
|
2246
|
+
resetToSource
|
|
2247
|
+
}),
|
|
2248
|
+
removedChildren
|
|
2924
2249
|
};
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
stableId: m.stableId,
|
|
2938
|
-
meta: m
|
|
2939
|
-
};
|
|
2940
|
-
};
|
|
2941
|
-
/** Direct children of a device: the union of the live registry's children
|
|
2942
|
-
* and the persisted-meta scan (`parentDeviceId === parentId`), deduplicated
|
|
2943
|
-
* and excluding self. Shared by the `remove` cascade and the `resetToSource`
|
|
2944
|
-
* resync purge (#19). */
|
|
2945
|
-
directChildIds = async (parentId) => {
|
|
2946
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2947
|
-
if (this.registry) for (const c of this.registry.getChildren(parentId)) ids.add(c.id);
|
|
2948
|
-
const meta = await this.readMeta();
|
|
2949
|
-
for (const m of Object.values(meta)) if (m.parentDeviceId === parentId) ids.add(m.id);
|
|
2950
|
-
ids.delete(parentId);
|
|
2951
|
-
return [...ids];
|
|
2250
|
+
}
|
|
2251
|
+
async function testField(pctx, input) {
|
|
2252
|
+
const { deviceId } = input;
|
|
2253
|
+
let owningAddonId = null;
|
|
2254
|
+
if (pctx.registry) owningAddonId = pctx.registry.getAddonId(deviceId);
|
|
2255
|
+
if (!owningAddonId) owningAddonId = (await pctx.metaStore.resolvePersistedById(deviceId))?.addonId ?? null;
|
|
2256
|
+
if (!owningAddonId) throw new Error(`Device with id ${deviceId} not found`);
|
|
2257
|
+
const dp = await pctx.host.waitDeviceProvider(owningAddonId);
|
|
2258
|
+
if (!dp) return {
|
|
2259
|
+
status: "ok",
|
|
2260
|
+
labels: [],
|
|
2261
|
+
error: void 0
|
|
2952
2262
|
};
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
const wanted = `${parentStableId}-${sourceKey}`;
|
|
2958
|
-
for (const m of Object.values(meta)) if (m.stableId === wanted) return m.id;
|
|
2959
|
-
return null;
|
|
2263
|
+
if (typeof dp.testCreationField !== "function") return {
|
|
2264
|
+
status: "ok",
|
|
2265
|
+
labels: [],
|
|
2266
|
+
error: void 0
|
|
2960
2267
|
};
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2268
|
+
return dp.testCreationField({
|
|
2269
|
+
type: DeviceType.Camera,
|
|
2270
|
+
key: input.key,
|
|
2271
|
+
value: input.value
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
//#endregion
|
|
2275
|
+
//#region src/builtins/device-manager/device-linked-devices.ts
|
|
2276
|
+
/** Reserved device-config keys (cf. `_profileMap`). */
|
|
2277
|
+
var LINKED_MODE_CONFIG_KEY = "_linkedDevicesMode";
|
|
2278
|
+
var LINKED_IDS_CONFIG_KEY = "_linkedDeviceIds";
|
|
2279
|
+
/** Parse the two reserved keys out of a device-config blob. Tolerant:
|
|
2280
|
+
* ids may arrive as numbers or numeric strings (the multiselect field
|
|
2281
|
+
* stores string option values); anything else is dropped. */
|
|
2282
|
+
function parseLinkedDevicesConfig(blob) {
|
|
2283
|
+
const mode = blob["_linkedDevicesMode"] === "manual" ? "manual" : "auto";
|
|
2284
|
+
const rawIds = blob[LINKED_IDS_CONFIG_KEY];
|
|
2285
|
+
const manualIds = [];
|
|
2286
|
+
if (Array.isArray(rawIds)) for (const v of rawIds) {
|
|
2287
|
+
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
2288
|
+
if (Number.isInteger(n) && !manualIds.includes(n)) manualIds.push(n);
|
|
2289
|
+
}
|
|
2290
|
+
return {
|
|
2291
|
+
mode,
|
|
2292
|
+
manualIds
|
|
2969
2293
|
};
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Pure resolution core: children ∪ (auto → same-location | manual → list),
|
|
2297
|
+
* minus self, deduped. Order: children first, then mode additions.
|
|
2298
|
+
*/
|
|
2299
|
+
function resolveLinkedDeviceIds(params) {
|
|
2300
|
+
const { selfId, selfLocation, config, candidates } = params;
|
|
2301
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2302
|
+
for (const c of candidates) if (c.parentDeviceId === selfId) ids.add(c.id);
|
|
2303
|
+
if (config.mode === "auto") {
|
|
2304
|
+
if (selfLocation !== null && selfLocation.length > 0) {
|
|
2305
|
+
for (const c of candidates) if (c.location === selfLocation) ids.add(c.id);
|
|
2306
|
+
}
|
|
2307
|
+
} else {
|
|
2308
|
+
const known = new Set(candidates.map((c) => c.id));
|
|
2309
|
+
for (const id of config.manualIds) if (known.has(id)) ids.add(id);
|
|
2310
|
+
}
|
|
2311
|
+
ids.delete(selfId);
|
|
2312
|
+
return [...ids];
|
|
2313
|
+
}
|
|
2314
|
+
/** Cap implementation of `getLinkedDevices`. */
|
|
2315
|
+
async function getLinkedDevices(pctx, input) {
|
|
2316
|
+
const { deviceId } = input;
|
|
2317
|
+
const [all, blob] = await Promise.all([listAll(pctx, {}), pctx.settings.readDeviceStore(deviceId)]);
|
|
2318
|
+
const config = parseLinkedDevicesConfig(blob);
|
|
2319
|
+
const byId = new Map(all.map((d) => [d.id, d]));
|
|
2320
|
+
const ids = resolveLinkedDeviceIds({
|
|
2321
|
+
selfId: deviceId,
|
|
2322
|
+
selfLocation: byId.get(deviceId)?.location ?? null,
|
|
2323
|
+
config,
|
|
2324
|
+
candidates: all
|
|
2325
|
+
});
|
|
2326
|
+
const devices = [];
|
|
2327
|
+
for (const id of ids) {
|
|
2328
|
+
const d = byId.get(id);
|
|
2329
|
+
if (!d) continue;
|
|
2330
|
+
devices.push({
|
|
2331
|
+
deviceId: d.id,
|
|
2332
|
+
name: d.name,
|
|
2333
|
+
location: d.location,
|
|
2334
|
+
features: d.features
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
return {
|
|
2338
|
+
mode: config.mode,
|
|
2339
|
+
devices
|
|
2974
2340
|
};
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* "Linked devices" settings section for a CAMERA (null for every other
|
|
2344
|
+
* device type — the association is camera-anchored). Contributed through
|
|
2345
|
+
* `collectSystemDeviceContributions`; saves route through the existing
|
|
2346
|
+
* `writerCapName: 'device-manager'` path into the device config blob.
|
|
2347
|
+
*/
|
|
2348
|
+
async function buildLinkedDevicesContribution(pctx, deviceId) {
|
|
2349
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2350
|
+
if (!persisted || persisted.meta.type !== DeviceType.Camera) return null;
|
|
2351
|
+
const [all, blob] = await Promise.all([listAll(pctx, {}), pctx.settings.readDeviceStore(deviceId)]);
|
|
2352
|
+
const config = parseLinkedDevicesConfig(blob);
|
|
2353
|
+
const options = all.filter((d) => d.id !== deviceId).map((d) => ({
|
|
2354
|
+
value: String(d.id),
|
|
2355
|
+
label: d.location !== null ? `${d.name} (${d.location})` : d.name
|
|
2356
|
+
}));
|
|
2357
|
+
return { sections: [{
|
|
2358
|
+
id: "linked-devices",
|
|
2359
|
+
title: "Linked devices",
|
|
2360
|
+
description: "Companion devices associated with this camera. Device-tree children are always linked; auto mode also links every device sharing the camera location, manual mode links the picked list instead.",
|
|
2361
|
+
order: 70,
|
|
2362
|
+
fields: [{
|
|
2363
|
+
type: "select",
|
|
2364
|
+
key: LINKED_MODE_CONFIG_KEY,
|
|
2365
|
+
label: "Link mode",
|
|
2366
|
+
description: "Auto: children + same-location devices. Manual: children + the list below.",
|
|
2367
|
+
options: [{
|
|
2368
|
+
value: "auto",
|
|
2369
|
+
label: "Auto (location-based)"
|
|
2370
|
+
}, {
|
|
2371
|
+
value: "manual",
|
|
2372
|
+
label: "Manual"
|
|
2373
|
+
}],
|
|
2374
|
+
value: config.mode
|
|
2375
|
+
}, {
|
|
2376
|
+
type: "multiselect",
|
|
2377
|
+
key: LINKED_IDS_CONFIG_KEY,
|
|
2378
|
+
label: "Manually linked devices",
|
|
2379
|
+
description: "Used in manual mode (children stay linked regardless).",
|
|
2380
|
+
options,
|
|
2381
|
+
value: config.manualIds.map(String)
|
|
2382
|
+
}]
|
|
2383
|
+
}] };
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Direct `applyDeviceSettingsPatch` normalization for the two reserved keys.
|
|
2387
|
+
* The admin-ui save path reaches the device config blob via the
|
|
2388
|
+
* `writerCapName: 'device-manager'` routing (never this method), but the cap
|
|
2389
|
+
* contract includes the method — keep it correct for direct callers.
|
|
2390
|
+
*/
|
|
2391
|
+
async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
|
|
2392
|
+
const next = { ...await pctx.settings.readDeviceStore(deviceId) };
|
|
2393
|
+
if ("_linkedDevicesMode" in patch) next[LINKED_MODE_CONFIG_KEY] = patch["_linkedDevicesMode"] === "manual" ? "manual" : "auto";
|
|
2394
|
+
if ("_linkedDeviceIds" in patch) next[LINKED_IDS_CONFIG_KEY] = [...parseLinkedDevicesConfig({ [LINKED_IDS_CONFIG_KEY]: patch[LINKED_IDS_CONFIG_KEY] }).manualIds];
|
|
2395
|
+
await pctx.settings.writeDeviceStore(deviceId, next);
|
|
2396
|
+
return { success: true };
|
|
2397
|
+
}
|
|
2398
|
+
//#endregion
|
|
2399
|
+
//#region src/builtins/device-manager/device-link-cycle.ts
|
|
2400
|
+
function nodeKey(deviceId, cap) {
|
|
2401
|
+
return `${deviceId}:${cap}`;
|
|
2402
|
+
}
|
|
2403
|
+
/** The target's container stableId — its parent's (falling back to its own for
|
|
2404
|
+
* a top-level target). Sibling FIELD sources resolve relative to this, the same
|
|
2405
|
+
* rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
|
|
2406
|
+
function containerStableId(targetRow, rowById) {
|
|
2407
|
+
return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
|
|
2408
|
+
}
|
|
2409
|
+
/** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
|
|
2410
|
+
* null when it is a literal or resolves to no known device. */
|
|
2411
|
+
function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
|
|
2412
|
+
if (binding.kind === "literal") return null;
|
|
2413
|
+
const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
|
|
2414
|
+
const srcId = idByStableId.get(wantedStableId);
|
|
2415
|
+
if (srcId === void 0) return null;
|
|
2416
|
+
return nodeKey(srcId, binding.cap);
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
|
|
2420
|
+
* A literal source depends on nothing; a field/global source depends on exactly
|
|
2421
|
+
* one node; an EXPRESSION source depends on one node per resolvable non-literal
|
|
2422
|
+
* binding. Unresolvable (dangling) bindings contribute no edge — they cannot
|
|
2423
|
+
* loop while dangling; a cycle that only becomes resolvable later is bounded by
|
|
2424
|
+
* the resolve-time re-entrancy guard.
|
|
2425
|
+
*/
|
|
2426
|
+
function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
|
|
2427
|
+
const src = link.source;
|
|
2428
|
+
if (src.kind === "expression") {
|
|
2429
|
+
const nodes = [];
|
|
2430
|
+
for (const binding of Object.values(src.bindings)) {
|
|
2431
|
+
const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
|
|
2432
|
+
if (node !== null) nodes.push(node);
|
|
2984
2433
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2434
|
+
return nodes;
|
|
2435
|
+
}
|
|
2436
|
+
const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
|
|
2437
|
+
return node !== null ? [node] : [];
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
|
|
2441
|
+
* a dependency cycle. Returns the cycle as an ordered list of node keys
|
|
2442
|
+
* (first node repeated at the end) or null when the set is safe.
|
|
2443
|
+
*/
|
|
2444
|
+
function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
|
|
2445
|
+
const rows = Object.values(allMeta);
|
|
2446
|
+
const rowById = /* @__PURE__ */ new Map();
|
|
2447
|
+
const idByStableId = /* @__PURE__ */ new Map();
|
|
2448
|
+
for (const r of rows) {
|
|
2449
|
+
rowById.set(r.id, r);
|
|
2450
|
+
if (!idByStableId.has(r.stableId)) idByStableId.set(r.stableId, r.id);
|
|
2451
|
+
}
|
|
2452
|
+
const dependsOn = /* @__PURE__ */ new Map();
|
|
2453
|
+
const addEdges = (row, links) => {
|
|
2454
|
+
for (const link of links) {
|
|
2455
|
+
const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
|
|
2456
|
+
if (srcNodes.length === 0) continue;
|
|
2457
|
+
const tNode = nodeKey(row.id, link.target.cap);
|
|
2458
|
+
const list = dependsOn.get(tNode) ?? [];
|
|
2459
|
+
for (const srcNode of srcNodes) list.push(srcNode);
|
|
2460
|
+
dependsOn.set(tNode, list);
|
|
2991
2461
|
}
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
}
|
|
3026
|
-
const wantedStableId = src.kind === "global" ? src.sourceStableId : `${container}-${src.sourceKey}`;
|
|
3027
|
-
expectedSources.add(wantedStableId);
|
|
3028
|
-
const srcId = idByStableId.get(wantedStableId);
|
|
3029
|
-
if (srcId === void 0) continue;
|
|
3030
|
-
entries.push({
|
|
3031
|
-
targetDeviceId: targetId,
|
|
3032
|
-
link,
|
|
3033
|
-
sourceDeviceId: srcId
|
|
3034
|
-
});
|
|
2462
|
+
};
|
|
2463
|
+
for (const r of rows) {
|
|
2464
|
+
const links = r.id === editedDeviceId ? editedLinks : r.deviceLinks ?? [];
|
|
2465
|
+
if (links.length > 0) addEdges(r, links);
|
|
2466
|
+
}
|
|
2467
|
+
const roots = [...new Set(editedLinks.map((l) => nodeKey(editedDeviceId, l.target.cap)))];
|
|
2468
|
+
for (const root of roots) {
|
|
2469
|
+
const cycle = dfsFindCycle(root, dependsOn);
|
|
2470
|
+
if (cycle) return cycle;
|
|
2471
|
+
}
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
/** Iterative DFS from `root` over `dependsOn`; returns the first back-edge
|
|
2475
|
+
* cycle path (closed — first node repeated last) or null. */
|
|
2476
|
+
function dfsFindCycle(root, dependsOn) {
|
|
2477
|
+
const onPath = /* @__PURE__ */ new Set();
|
|
2478
|
+
const done = /* @__PURE__ */ new Set();
|
|
2479
|
+
const path = [];
|
|
2480
|
+
const stack = [{
|
|
2481
|
+
node: root,
|
|
2482
|
+
nextChild: 0
|
|
2483
|
+
}];
|
|
2484
|
+
onPath.add(root);
|
|
2485
|
+
path.push(root);
|
|
2486
|
+
while (stack.length > 0) {
|
|
2487
|
+
const frame = stack[stack.length - 1];
|
|
2488
|
+
const children = dependsOn.get(frame.node) ?? [];
|
|
2489
|
+
if (frame.nextChild < children.length) {
|
|
2490
|
+
const child = children[frame.nextChild];
|
|
2491
|
+
frame.nextChild += 1;
|
|
2492
|
+
if (onPath.has(child)) {
|
|
2493
|
+
const start = path.indexOf(child);
|
|
2494
|
+
return [...path.slice(start), child];
|
|
3035
2495
|
}
|
|
2496
|
+
if (done.has(child)) continue;
|
|
2497
|
+
stack.push({
|
|
2498
|
+
node: child,
|
|
2499
|
+
nextChild: 0
|
|
2500
|
+
});
|
|
2501
|
+
onPath.add(child);
|
|
2502
|
+
path.push(child);
|
|
2503
|
+
continue;
|
|
3036
2504
|
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
2505
|
+
stack.pop();
|
|
2506
|
+
onPath.delete(frame.node);
|
|
2507
|
+
path.pop();
|
|
2508
|
+
done.add(frame.node);
|
|
2509
|
+
}
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
3041
2512
|
//#endregion
|
|
3042
|
-
//#region src/builtins/device-manager/device-
|
|
2513
|
+
//#region src/builtins/device-manager/device-meta-actions.ts
|
|
3043
2514
|
/**
|
|
3044
|
-
* Device
|
|
3045
|
-
*
|
|
3046
|
-
*
|
|
3047
|
-
*
|
|
3048
|
-
*
|
|
3049
|
-
*
|
|
2515
|
+
* Device meta-mutation + persistence actions for the device-manager addon —
|
|
2516
|
+
* the `IDeviceManagerProvider` write/load surface. Extracted verbatim from the
|
|
2517
|
+
* `onInitialize` provider literal as free functions taking a `ProviderContext`
|
|
2518
|
+
* (DI). Each function's input/return types are pinned to the cap contract via
|
|
2519
|
+
* `Parameters<IDeviceManagerProvider['x']>[0]` / `ReturnType<…>` so the typed
|
|
2520
|
+
* surface is identical to the inferred object-literal methods.
|
|
3050
2521
|
*
|
|
3051
|
-
* Covers:
|
|
3052
|
-
*
|
|
3053
|
-
*
|
|
3054
|
-
*
|
|
3055
|
-
*
|
|
3056
|
-
*
|
|
3057
|
-
* (
|
|
2522
|
+
* Covers: id allocation + registration (allocateDeviceId, registerDevice,
|
|
2523
|
+
* removeDevice), config persistence (persistConfig, loadConfig), the meta
|
|
2524
|
+
* surface load (loadMeta, loadRuntimeState), every meta setter (setName,
|
|
2525
|
+
* setLocation, setType, setIntegrationId, setLinkDeviceId,
|
|
2526
|
+
* setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
|
|
2527
|
+
* applyInitialMeta, setMetadata, setDisabled), and the location registry
|
|
2528
|
+
* (listLocations, addLocation, removeLocation).
|
|
3058
2529
|
*
|
|
3059
|
-
*
|
|
3060
|
-
*
|
|
3061
|
-
*
|
|
3062
|
-
*
|
|
3063
|
-
* `pctx.provider`. Pure refactor — no behavior change.
|
|
2530
|
+
* EXACT ordering and side effects preserved — notably registerDevice's
|
|
2531
|
+
* name-precedence reconcile + field-preservation spreads, the
|
|
2532
|
+
* withMetaWriteLock-serialised read-modify-writes, the event emissions, and the
|
|
2533
|
+
* link reverse-index rebuild gating. Pure refactor — no behavior change.
|
|
3064
2534
|
*/
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
2535
|
+
/**
|
|
2536
|
+
* Stamp (or update) the owning integration id on a device's meta row + emit
|
|
2537
|
+
* `DeviceMetaChanged`. The device-manager chokepoint shared by the
|
|
2538
|
+
* `setIntegrationId` cap handler AND the create/adopt forwarders (queries)
|
|
2539
|
+
* that stamp an integration's ownership onto every device it materializes.
|
|
2540
|
+
* Idempotent. Takes its low-level deps directly (not the full `ProviderContext`)
|
|
2541
|
+
* because `ProviderContext.stampIntegrationId` delegates HERE — passing the
|
|
2542
|
+
* context would be a capture cycle.
|
|
2543
|
+
*/
|
|
2544
|
+
async function stampIntegrationId(metaStore, settings, ctx, deviceId, integrationId) {
|
|
2545
|
+
await metaStore.withMetaWriteLock(async () => {
|
|
2546
|
+
const persisted = await metaStore.resolvePersistedById(deviceId);
|
|
2547
|
+
if (!persisted) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
|
|
2548
|
+
const { meta: m } = persisted;
|
|
2549
|
+
const key = String(deviceId);
|
|
2550
|
+
const allMeta = await metaStore.readMeta();
|
|
2551
|
+
await settings.writeAddonStore({ deviceMeta: {
|
|
2552
|
+
...allMeta,
|
|
2553
|
+
[key]: {
|
|
2554
|
+
...m,
|
|
2555
|
+
integrationId
|
|
2556
|
+
}
|
|
2557
|
+
} });
|
|
2558
|
+
});
|
|
2559
|
+
ctx.eventBus.emit({
|
|
2560
|
+
id: randomUUID(),
|
|
2561
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2562
|
+
source: {
|
|
2563
|
+
type: "device",
|
|
2564
|
+
id: deviceId
|
|
2565
|
+
},
|
|
2566
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2567
|
+
data: {
|
|
2568
|
+
deviceId,
|
|
2569
|
+
field: "integrationId",
|
|
2570
|
+
value: integrationId
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
async function allocateDeviceId(pctx, input) {
|
|
2575
|
+
const { addonId, stableId } = input;
|
|
2576
|
+
return await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2577
|
+
const meta = await pctx.metaStore.readMeta();
|
|
2578
|
+
const existing = Object.values(meta).find((m) => m.addonId === addonId && m.stableId === stableId);
|
|
2579
|
+
if (existing) return { id: existing.id };
|
|
2580
|
+
const id = await pctx.metaStore.allocateNextDeviceId();
|
|
2581
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2582
|
+
...meta,
|
|
2583
|
+
[String(id)]: {
|
|
2584
|
+
addonId,
|
|
2585
|
+
stableId,
|
|
2586
|
+
type: "generic",
|
|
2587
|
+
name: stableId,
|
|
2588
|
+
location: null,
|
|
2589
|
+
disabled: false,
|
|
2590
|
+
parentDeviceId: null,
|
|
2591
|
+
id
|
|
2592
|
+
}
|
|
2593
|
+
} });
|
|
2594
|
+
return { id };
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
async function registerDevice(pctx, input) {
|
|
2598
|
+
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
2599
|
+
const key = String(id);
|
|
2600
|
+
const featuresArr = Array.isArray(features) ? [...features] : [];
|
|
2601
|
+
const { isFirstRegistration, exportFingerprint, fingerprintChanged } = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2602
|
+
const index = await pctx.metaStore.readIndex();
|
|
2603
|
+
const existing = index[addonId] ?? [];
|
|
2604
|
+
const wasInIndex = existing.includes(stableId);
|
|
2605
|
+
if (!wasInIndex) await pctx.settings.writeAddonStore({ deviceIndex: {
|
|
2606
|
+
...index,
|
|
2607
|
+
[addonId]: [...existing, stableId]
|
|
2608
|
+
} });
|
|
2609
|
+
const meta = await pctx.metaStore.readMeta();
|
|
2610
|
+
const existingMeta = meta[key];
|
|
2611
|
+
const wasUserNamed = existingMeta?.userNamed ?? (existingMeta !== void 0 && existingMeta.name !== stableId);
|
|
2612
|
+
const resolvedName = wasUserNamed && existingMeta !== void 0 ? existingMeta.name : name;
|
|
2613
|
+
const isFirst = !existingMeta || !wasInIndex;
|
|
2614
|
+
const fingerprint = canonicalDeviceFingerprint({
|
|
2615
|
+
deviceId: id,
|
|
2616
|
+
deviceType: type,
|
|
2617
|
+
features: featuresArr
|
|
2618
|
+
});
|
|
2619
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2620
|
+
...meta,
|
|
2621
|
+
[key]: {
|
|
2622
|
+
addonId,
|
|
2623
|
+
stableId,
|
|
2624
|
+
type,
|
|
2625
|
+
name: resolvedName,
|
|
2626
|
+
userNamed: wasUserNamed,
|
|
2627
|
+
location: existingMeta?.location ?? null,
|
|
2628
|
+
disabled: existingMeta?.disabled ?? false,
|
|
2629
|
+
...existingMeta?.integrationId !== void 0 ? { integrationId: existingMeta.integrationId } : {},
|
|
2630
|
+
...existingMeta?.linkDeviceId !== void 0 ? { linkDeviceId: existingMeta.linkDeviceId } : {},
|
|
2631
|
+
...existingMeta?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: existingMeta.primaryChildEntityId } : {},
|
|
2632
|
+
...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
|
|
2633
|
+
...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
|
|
2634
|
+
...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
|
|
2635
|
+
...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
|
|
2636
|
+
parentDeviceId,
|
|
2637
|
+
id,
|
|
2638
|
+
features: featuresArr,
|
|
2639
|
+
exportFingerprint: fingerprint
|
|
2640
|
+
}
|
|
2641
|
+
} });
|
|
3073
2642
|
return {
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
name: m.name,
|
|
3078
|
-
location: m.location ?? null,
|
|
3079
|
-
disabled: m.disabled ?? false,
|
|
3080
|
-
parentDeviceId: m.parentDeviceId
|
|
2643
|
+
isFirstRegistration: isFirst,
|
|
2644
|
+
exportFingerprint: fingerprint,
|
|
2645
|
+
fingerprintChanged: existingMeta?.exportFingerprint !== fingerprint
|
|
3081
2646
|
};
|
|
3082
2647
|
});
|
|
2648
|
+
if (Object.keys(config).length > 0) await pctx.settings.writeDeviceStore(id, config);
|
|
2649
|
+
pctx.metaStore.idToAddonId.set(id, addonId);
|
|
2650
|
+
if (isFirstRegistration) pctx.host.ctx.eventBus.emit({
|
|
2651
|
+
id: randomUUID(),
|
|
2652
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2653
|
+
source: {
|
|
2654
|
+
type: "device",
|
|
2655
|
+
id
|
|
2656
|
+
},
|
|
2657
|
+
category: EventCategory.DeviceRegistered,
|
|
2658
|
+
data: {
|
|
2659
|
+
deviceId: id,
|
|
2660
|
+
name: name.length > 0 ? name : stableId,
|
|
2661
|
+
providerId: addonId,
|
|
2662
|
+
parentDeviceId: parentDeviceId ?? null
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
else pctx.host.ctx.eventBus.emit({
|
|
2666
|
+
id: randomUUID(),
|
|
2667
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2668
|
+
source: {
|
|
2669
|
+
type: "device",
|
|
2670
|
+
id
|
|
2671
|
+
},
|
|
2672
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2673
|
+
data: {
|
|
2674
|
+
deviceId: id,
|
|
2675
|
+
name: name.length > 0 ? name : stableId,
|
|
2676
|
+
providerId: addonId,
|
|
2677
|
+
parentDeviceId: parentDeviceId ?? null,
|
|
2678
|
+
features: featuresArr
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
if (isFirstRegistration || fingerprintChanged) pctx.host.ctx.eventBus.emit({
|
|
2682
|
+
id: randomUUID(),
|
|
2683
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2684
|
+
source: {
|
|
2685
|
+
type: "device",
|
|
2686
|
+
id
|
|
2687
|
+
},
|
|
2688
|
+
category: EventCategory.DeviceProvisioned,
|
|
2689
|
+
data: {
|
|
2690
|
+
deviceId: id,
|
|
2691
|
+
fingerprint: exportFingerprint,
|
|
2692
|
+
generation: Date.now()
|
|
2693
|
+
}
|
|
2694
|
+
});
|
|
2695
|
+
if (pctx.host.devicesWithLinks.size > 0 && pctx.host.expectedSourceStableIds.has(stableId)) await pctx.metaStore.rebuildLinkDependents();
|
|
3083
2696
|
}
|
|
3084
|
-
async function
|
|
3085
|
-
const {
|
|
3086
|
-
const
|
|
3087
|
-
|
|
3088
|
-
const meta =
|
|
3089
|
-
const
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3100
|
-
|
|
2697
|
+
async function removeDevice(pctx, input) {
|
|
2698
|
+
const { deviceId } = input;
|
|
2699
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2700
|
+
if (!persisted) return;
|
|
2701
|
+
const { addonId, stableId, meta: persistedMeta } = persisted;
|
|
2702
|
+
const key = String(deviceId);
|
|
2703
|
+
const deviceName = persistedMeta.name;
|
|
2704
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2705
|
+
const index = await pctx.metaStore.readIndex();
|
|
2706
|
+
const remaining = (index[addonId] ?? []).filter((sid) => sid !== stableId);
|
|
2707
|
+
const updatedIndex = remaining.length > 0 ? {
|
|
2708
|
+
...index,
|
|
2709
|
+
[addonId]: remaining
|
|
2710
|
+
} : (() => {
|
|
2711
|
+
const { [addonId]: _removed, ...rest } = index;
|
|
2712
|
+
return rest;
|
|
2713
|
+
})();
|
|
2714
|
+
await pctx.settings.writeAddonStore({ deviceIndex: updatedIndex });
|
|
2715
|
+
const { [key]: _removedMeta, ...restMeta } = await pctx.metaStore.readMeta();
|
|
2716
|
+
await pctx.settings.writeAddonStore({ deviceMeta: restMeta });
|
|
2717
|
+
const map = await pctx.metaStore.readMetadataMap();
|
|
2718
|
+
if (key in map) {
|
|
2719
|
+
const { [key]: _removedMetadata, ...restMap } = map;
|
|
2720
|
+
await pctx.settings.writeAddonStore({ deviceMetadata: restMap });
|
|
3101
2721
|
}
|
|
2722
|
+
});
|
|
2723
|
+
await pctx.settings.clearDeviceStore(deviceId);
|
|
2724
|
+
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
2725
|
+
const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
|
|
2726
|
+
const bindingKey = String(deviceId);
|
|
2727
|
+
if (bindingsStore.deviceBindings[bindingKey]) {
|
|
2728
|
+
const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
|
|
2729
|
+
await writeBindingsStore(pctx.bindingsDeps, { deviceBindings: restBindings });
|
|
3102
2730
|
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
isCamera: persistedType === DeviceType.Camera,
|
|
3128
|
-
config: persistedConfig ?? {},
|
|
3129
|
-
metadata,
|
|
3130
|
-
...m?.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
3131
|
-
...m?.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
3132
|
-
...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
3133
|
-
...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
3134
|
-
...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
3135
|
-
...m?.display !== void 0 ? { display: m.display } : {},
|
|
3136
|
-
...(() => {
|
|
3137
|
-
const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
3138
|
-
return si !== void 0 ? { sourceInfo: si } : {};
|
|
3139
|
-
})()
|
|
3140
|
-
});
|
|
3141
|
-
}
|
|
3142
|
-
return results;
|
|
2731
|
+
pctx.host.remoteNativeCaps.delete(deviceId);
|
|
2732
|
+
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
2733
|
+
pctx.metaStore.idToAddonId.delete(deviceId);
|
|
2734
|
+
pctx.host.devicesWithLinks.delete(deviceId);
|
|
2735
|
+
pctx.host.dropDeviceOverlays(deviceId);
|
|
2736
|
+
await pctx.metaStore.rebuildLinkDependents();
|
|
2737
|
+
pctx.host.ctx.logger.info("removed device", { tags: {
|
|
2738
|
+
deviceId,
|
|
2739
|
+
deviceName: deviceName.length > 0 ? deviceName : stableId
|
|
2740
|
+
} });
|
|
2741
|
+
pctx.host.ctx.eventBus.emit({
|
|
2742
|
+
id: randomUUID(),
|
|
2743
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2744
|
+
source: {
|
|
2745
|
+
type: "device",
|
|
2746
|
+
id: deviceId
|
|
2747
|
+
},
|
|
2748
|
+
category: EventCategory.DeviceUnregistered,
|
|
2749
|
+
data: {
|
|
2750
|
+
deviceId,
|
|
2751
|
+
providerId: addonId,
|
|
2752
|
+
parentDeviceId: persistedMeta.parentDeviceId ?? null
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
3143
2755
|
}
|
|
3144
|
-
async function
|
|
2756
|
+
async function persistConfig(pctx, input) {
|
|
2757
|
+
const { deviceId, data } = input;
|
|
2758
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] persistConfig: unknown device id=${deviceId}`);
|
|
2759
|
+
await pctx.settings.writeDeviceStore(deviceId, data);
|
|
2760
|
+
}
|
|
2761
|
+
async function loadConfig(pctx, input) {
|
|
2762
|
+
const { deviceId } = input;
|
|
2763
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
2764
|
+
return pctx.settings.readDeviceStore(deviceId);
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Load the operator-organisational meta surface for one device
|
|
2768
|
+
* (`name` / `location` / `disabled` / `type` / `parentDeviceId`
|
|
2769
|
+
* / `addonId` + `id` / `stableId`). Used by the kernel proxy's
|
|
2770
|
+
* device-context factory to populate `ctx.deviceMeta` before
|
|
2771
|
+
* the device class constructor runs. Returns `null` when no
|
|
2772
|
+
* persisted row exists for the id.
|
|
2773
|
+
*
|
|
2774
|
+
* Reads default `location` to `null` and `disabled` to `false`
|
|
2775
|
+
* for legacy rows that predate the field — production code
|
|
2776
|
+
* relies on the IDevice type contract that both are present.
|
|
2777
|
+
*/
|
|
2778
|
+
async function loadMeta(pctx, input) {
|
|
3145
2779
|
const { deviceId } = input;
|
|
3146
|
-
if (pctx.registry) {
|
|
3147
|
-
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
3148
|
-
if (found) {
|
|
3149
|
-
const key = String(found.device.id);
|
|
3150
|
-
const [map, metaMap] = await Promise.all([pctx.metaStore.readMetadataMap(), pctx.metaStore.readMeta()]);
|
|
3151
|
-
const metadata = map[key] ?? null;
|
|
3152
|
-
const metaRow = metaMap[key] ?? null;
|
|
3153
|
-
return toDeviceInfo(found.addonId, found.device, metadata, metaRow);
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
2780
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3157
2781
|
if (!persisted) return null;
|
|
3158
|
-
const { addonId
|
|
3159
|
-
const persistedConfig = await pctx.settings.readDeviceStore(m.id);
|
|
2782
|
+
const { addonId, stableId, meta: m } = persisted;
|
|
3160
2783
|
const key = String(deviceId);
|
|
3161
2784
|
const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
|
|
3162
|
-
const sourceInfoGetDevice = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
3163
2785
|
return {
|
|
3164
|
-
id:
|
|
2786
|
+
id: m.id,
|
|
3165
2787
|
stableId,
|
|
3166
|
-
addonId
|
|
2788
|
+
addonId,
|
|
3167
2789
|
type: m.type,
|
|
3168
2790
|
name: m.name,
|
|
3169
2791
|
location: m.location ?? null,
|
|
3170
2792
|
disabled: m.disabled ?? false,
|
|
3171
2793
|
parentDeviceId: m.parentDeviceId,
|
|
3172
|
-
role: toDeviceRole(m.role),
|
|
3173
|
-
online: pctx.host.resolveDeviceOnline(deviceId, true),
|
|
3174
|
-
probed: pctx.host.resolveDeviceProbed(deviceId),
|
|
3175
|
-
features: persistedFeatures(m.features),
|
|
3176
|
-
isCamera: false,
|
|
3177
|
-
config: persistedConfig ?? {},
|
|
3178
2794
|
metadata,
|
|
3179
2795
|
...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
3180
2796
|
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
3181
|
-
...m.
|
|
3182
|
-
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
3183
|
-
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
3184
|
-
...m.display !== void 0 ? { display: m.display } : {},
|
|
3185
|
-
...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
|
|
2797
|
+
...m.role !== void 0 ? { role: m.role } : {}
|
|
3186
2798
|
};
|
|
3187
2799
|
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
pctx.metaStore.
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
2800
|
+
/**
|
|
2801
|
+
* Update the operator-edited display name. Writes the meta
|
|
2802
|
+
* row, emits a `DeviceMetaChanged` event so live consumers
|
|
2803
|
+
* (UI device list, alert center) see the rename without
|
|
2804
|
+
* polling. The live `IDevice.name` mirror is updated by the
|
|
2805
|
+
* kernel proxy on its side (`device-cap-proxy.ts`).
|
|
2806
|
+
*/
|
|
2807
|
+
async function setName(pctx, input) {
|
|
2808
|
+
const { deviceId, name } = input;
|
|
2809
|
+
const cascaded = [];
|
|
2810
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2811
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2812
|
+
if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
|
|
2813
|
+
const { meta: m } = persisted;
|
|
2814
|
+
const key = String(deviceId);
|
|
2815
|
+
const oldName = m.name;
|
|
2816
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2817
|
+
const nextMeta = {
|
|
2818
|
+
...allMeta,
|
|
2819
|
+
[key]: {
|
|
2820
|
+
...m,
|
|
2821
|
+
name,
|
|
2822
|
+
userNamed: true
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
if (oldName.length > 0 && oldName !== name) for (const [childKey, childMeta] of Object.entries(allMeta)) {
|
|
2826
|
+
if (childKey === key) continue;
|
|
2827
|
+
if (!(childMeta.parentDeviceId === deviceId || childMeta.linkDeviceId === deviceId)) continue;
|
|
2828
|
+
if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
|
|
2829
|
+
const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
|
|
2830
|
+
nextMeta[childKey] = {
|
|
2831
|
+
...childMeta,
|
|
2832
|
+
name: childName
|
|
2833
|
+
};
|
|
2834
|
+
cascaded.push({
|
|
2835
|
+
id: Number(childKey),
|
|
2836
|
+
name: childName
|
|
2837
|
+
});
|
|
3214
2838
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
3246
|
-
...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
|
|
3247
|
-
...m.display !== void 0 ? { display: m.display } : {},
|
|
3248
|
-
...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
|
|
3249
|
-
});
|
|
3250
|
-
}
|
|
3251
|
-
return results;
|
|
2839
|
+
await pctx.settings.writeAddonStore({ deviceMeta: nextMeta });
|
|
2840
|
+
});
|
|
2841
|
+
pctx.host.ctx.eventBus.emit({
|
|
2842
|
+
id: randomUUID(),
|
|
2843
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2844
|
+
source: {
|
|
2845
|
+
type: "device",
|
|
2846
|
+
id: deviceId
|
|
2847
|
+
},
|
|
2848
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2849
|
+
data: {
|
|
2850
|
+
deviceId,
|
|
2851
|
+
field: "name",
|
|
2852
|
+
value: name
|
|
2853
|
+
}
|
|
2854
|
+
});
|
|
2855
|
+
for (const child of cascaded) pctx.host.ctx.eventBus.emit({
|
|
2856
|
+
id: randomUUID(),
|
|
2857
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2858
|
+
source: {
|
|
2859
|
+
type: "device",
|
|
2860
|
+
id: child.id
|
|
2861
|
+
},
|
|
2862
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2863
|
+
data: {
|
|
2864
|
+
deviceId: child.id,
|
|
2865
|
+
field: "name",
|
|
2866
|
+
value: child.name
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
3252
2869
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
2870
|
+
/**
|
|
2871
|
+
* Update the operator-organisational location label. `null`
|
|
2872
|
+
* clears it. Mirrors the same persist-then-emit shape as
|
|
2873
|
+
* `setName`; consumers subscribe to `DeviceMetaChanged` and
|
|
2874
|
+
* filter on `field: 'location'`.
|
|
2875
|
+
*/
|
|
2876
|
+
async function setLocation(pctx, input) {
|
|
2877
|
+
const { deviceId, location } = input;
|
|
2878
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2879
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2880
|
+
if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
|
|
2881
|
+
const { meta: m } = persisted;
|
|
2882
|
+
const key = String(deviceId);
|
|
2883
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2884
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2885
|
+
...allMeta,
|
|
2886
|
+
[key]: {
|
|
2887
|
+
...m,
|
|
2888
|
+
location
|
|
2889
|
+
}
|
|
2890
|
+
} });
|
|
2891
|
+
});
|
|
2892
|
+
pctx.host.ctx.eventBus.emit({
|
|
2893
|
+
id: randomUUID(),
|
|
2894
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2895
|
+
source: {
|
|
2896
|
+
type: "device",
|
|
2897
|
+
id: deviceId
|
|
2898
|
+
},
|
|
2899
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2900
|
+
data: {
|
|
2901
|
+
deviceId,
|
|
2902
|
+
field: "location",
|
|
2903
|
+
value: location
|
|
3270
2904
|
}
|
|
3271
|
-
}
|
|
3272
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
|
|
3273
|
-
return (await pctx.requireDeviceOps(deviceId).getStreamSources({ deviceId })).map((s) => ({ ...s }));
|
|
2905
|
+
});
|
|
3274
2906
|
}
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
2907
|
+
/**
|
|
2908
|
+
* Update the device type. Writes the meta row, emits a
|
|
2909
|
+
* `DeviceMetaChanged` event. Used by the kernel to apply
|
|
2910
|
+
* `initialMeta.type` before device construction so the device
|
|
2911
|
+
* constructs with its real type instead of the `allocateDeviceId`
|
|
2912
|
+
* placeholder `'generic'`.
|
|
2913
|
+
*/
|
|
2914
|
+
async function setType(pctx, input) {
|
|
2915
|
+
const { deviceId, type } = input;
|
|
2916
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2917
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2918
|
+
if (!persisted) throw new Error(`[device-manager] setType: unknown device id=${deviceId}`);
|
|
2919
|
+
const { meta: m } = persisted;
|
|
2920
|
+
const key = String(deviceId);
|
|
2921
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2922
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2923
|
+
...allMeta,
|
|
2924
|
+
[key]: {
|
|
2925
|
+
...m,
|
|
2926
|
+
type
|
|
2927
|
+
}
|
|
2928
|
+
} });
|
|
2929
|
+
});
|
|
2930
|
+
pctx.host.ctx.eventBus.emit({
|
|
2931
|
+
id: randomUUID(),
|
|
2932
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2933
|
+
source: {
|
|
2934
|
+
type: "device",
|
|
2935
|
+
id: deviceId
|
|
2936
|
+
},
|
|
2937
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2938
|
+
data: {
|
|
2939
|
+
deviceId,
|
|
2940
|
+
field: "type",
|
|
2941
|
+
value: type
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
3287
2944
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
2945
|
+
/**
|
|
2946
|
+
* Stamp (or update) the owning integration id on the device's
|
|
2947
|
+
* meta row. Called by the kernel's `create()` pre-seed path
|
|
2948
|
+
* when `initialMeta.integrationId` is set (analogous to
|
|
2949
|
+
* `setName` / `setType`). Idempotent.
|
|
2950
|
+
*/
|
|
2951
|
+
async function setIntegrationId(pctx, input) {
|
|
2952
|
+
await pctx.stampIntegrationId(input.deviceId, input.integrationId);
|
|
3296
2953
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
2954
|
+
/**
|
|
2955
|
+
* Stamp (or update) the linked device id on the device's meta
|
|
2956
|
+
* row. Called by the kernel's `create()` pre-seed path when
|
|
2957
|
+
* `initialMeta.linkDeviceId` is set (analogous to `setIntegrationId`).
|
|
2958
|
+
* Idempotent. `null` explicitly clears a previous link.
|
|
2959
|
+
*/
|
|
2960
|
+
async function setLinkDeviceId(pctx, input) {
|
|
2961
|
+
const { deviceId, linkDeviceId } = input;
|
|
2962
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2963
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2964
|
+
if (!persisted) throw new Error(`[device-manager] setLinkDeviceId: unknown device id=${deviceId}`);
|
|
2965
|
+
const { meta: m } = persisted;
|
|
2966
|
+
const key = String(deviceId);
|
|
2967
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
2968
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
2969
|
+
...allMeta,
|
|
2970
|
+
[key]: {
|
|
2971
|
+
...m,
|
|
2972
|
+
linkDeviceId
|
|
2973
|
+
}
|
|
2974
|
+
} });
|
|
2975
|
+
});
|
|
2976
|
+
pctx.host.ctx.eventBus.emit({
|
|
2977
|
+
id: randomUUID(),
|
|
2978
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2979
|
+
source: {
|
|
2980
|
+
type: "device",
|
|
2981
|
+
id: deviceId
|
|
2982
|
+
},
|
|
2983
|
+
category: EventCategory.DeviceMetaChanged,
|
|
2984
|
+
data: {
|
|
2985
|
+
deviceId,
|
|
2986
|
+
field: "linkDeviceId",
|
|
2987
|
+
value: linkDeviceId
|
|
3304
2988
|
}
|
|
3305
|
-
}
|
|
3306
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
|
|
3307
|
-
await pctx.requireDeviceOps(deviceId).setConfig({
|
|
3308
|
-
deviceId,
|
|
3309
|
-
values: input.values
|
|
3310
2989
|
});
|
|
3311
|
-
return { success: true };
|
|
3312
2990
|
}
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
2991
|
+
/**
|
|
2992
|
+
* Set (or clear) the durable primary-child override on a CONTAINER
|
|
2993
|
+
* device's meta row, keyed on the chosen child's re-sync/rename-stable
|
|
2994
|
+
* `entityId`. Mirrors `setLinkDeviceId` but persists a string entityId
|
|
2995
|
+
* (survives a re-sync that reallocates the child's numeric id) instead
|
|
2996
|
+
* of the numeric child id. `null` clears the override → priority default.
|
|
2997
|
+
*/
|
|
2998
|
+
async function setPrimaryChildEntityId(pctx, input) {
|
|
2999
|
+
const { deviceId, primaryChildEntityId } = input;
|
|
3000
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3001
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3002
|
+
if (!persisted) throw new Error(`[device-manager] setPrimaryChildEntityId: unknown device id=${deviceId}`);
|
|
3003
|
+
const { meta: m } = persisted;
|
|
3004
|
+
const key = String(deviceId);
|
|
3005
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
3006
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3007
|
+
...allMeta,
|
|
3008
|
+
[key]: {
|
|
3009
|
+
...m,
|
|
3010
|
+
primaryChildEntityId
|
|
3011
|
+
}
|
|
3012
|
+
} });
|
|
3013
|
+
});
|
|
3014
|
+
pctx.host.ctx.eventBus.emit({
|
|
3015
|
+
id: randomUUID(),
|
|
3016
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3017
|
+
source: {
|
|
3018
|
+
type: "device",
|
|
3019
|
+
id: deviceId
|
|
3020
|
+
},
|
|
3021
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3022
|
+
data: {
|
|
3023
|
+
deviceId,
|
|
3024
|
+
field: "primaryChildEntityId",
|
|
3025
|
+
value: primaryChildEntityId
|
|
3026
|
+
}
|
|
3317
3027
|
});
|
|
3318
|
-
return { success: true };
|
|
3319
3028
|
}
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3029
|
+
/**
|
|
3030
|
+
* Set (or replace) the container-level `childLayout` on a device's
|
|
3031
|
+
* meta row. Mirrors `setPrimaryChildEntityId` but persists the
|
|
3032
|
+
* accordion-section layout array (parent-only). Persisted, projected,
|
|
3033
|
+
* and preserved across re-register/restore.
|
|
3034
|
+
*/
|
|
3035
|
+
async function setChildLayout(pctx, input) {
|
|
3036
|
+
const { deviceId, childLayout } = input;
|
|
3037
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3038
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3039
|
+
if (!persisted) throw new Error(`[device-manager] setChildLayout: unknown device id=${deviceId}`);
|
|
3040
|
+
const { meta: m } = persisted;
|
|
3041
|
+
const key = String(deviceId);
|
|
3042
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
3043
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3044
|
+
...allMeta,
|
|
3045
|
+
[key]: {
|
|
3046
|
+
...m,
|
|
3047
|
+
childLayout
|
|
3048
|
+
}
|
|
3049
|
+
} });
|
|
3050
|
+
});
|
|
3051
|
+
pctx.host.ctx.eventBus.emit({
|
|
3052
|
+
id: randomUUID(),
|
|
3053
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3054
|
+
source: {
|
|
3055
|
+
type: "device",
|
|
3056
|
+
id: deviceId
|
|
3057
|
+
},
|
|
3058
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3059
|
+
data: {
|
|
3060
|
+
deviceId,
|
|
3061
|
+
field: "childLayout",
|
|
3062
|
+
value: childLayout
|
|
3063
|
+
}
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Set (or replace) the operator-authored cross-device field wirings on
|
|
3068
|
+
* a device's meta row. Mirrors `setChildLayout` but persists the
|
|
3069
|
+
* `deviceLinks` array. Persisted, projected, and preserved across
|
|
3070
|
+
* re-register/restore. Also maintains the in-memory `devicesWithLinks`
|
|
3071
|
+
* Set used as an O(1) gate by `resolveLinkedStatus`.
|
|
3072
|
+
*/
|
|
3073
|
+
async function setDeviceLinks(pctx, input) {
|
|
3074
|
+
const { deviceId, deviceLinks } = input;
|
|
3075
|
+
const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
|
|
3076
|
+
if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
|
|
3077
|
+
for (const link of deviceLinks) {
|
|
3078
|
+
if (link.source.kind !== "expression") continue;
|
|
3079
|
+
const err = validateExpressionSource(link.source);
|
|
3080
|
+
if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
|
|
3081
|
+
}
|
|
3082
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3083
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3084
|
+
if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
|
|
3085
|
+
const { meta: m } = persisted;
|
|
3086
|
+
const key = String(deviceId);
|
|
3087
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
3088
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3089
|
+
...allMeta,
|
|
3090
|
+
[key]: {
|
|
3091
|
+
...m,
|
|
3092
|
+
deviceLinks
|
|
3093
|
+
}
|
|
3094
|
+
} });
|
|
3095
|
+
});
|
|
3096
|
+
if (deviceLinks.length > 0) pctx.host.devicesWithLinks.add(deviceId);
|
|
3097
|
+
else pctx.host.devicesWithLinks.delete(deviceId);
|
|
3098
|
+
await pctx.metaStore.rebuildLinkDependents();
|
|
3099
|
+
pctx.host.ctx.eventBus.emit({
|
|
3100
|
+
id: randomUUID(),
|
|
3101
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3102
|
+
source: {
|
|
3103
|
+
type: "device",
|
|
3104
|
+
id: deviceId
|
|
3105
|
+
},
|
|
3106
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3107
|
+
data: {
|
|
3108
|
+
deviceId,
|
|
3109
|
+
field: "deviceLinks",
|
|
3110
|
+
value: deviceLinks
|
|
3111
|
+
}
|
|
3324
3112
|
});
|
|
3325
|
-
return { success: true };
|
|
3326
3113
|
}
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3114
|
+
/**
|
|
3115
|
+
* Stamp (or update) the semantic role on the device's meta row.
|
|
3116
|
+
* Called by the kernel's `create()` / `spawnAccessoryChild`
|
|
3117
|
+
* pre-seed when `initialMeta.role` is set (analogous to
|
|
3118
|
+
* `setIntegrationId`). Idempotent. `null` clears a previous role.
|
|
3119
|
+
*/
|
|
3120
|
+
async function setRole(pctx, input) {
|
|
3121
|
+
const { deviceId, role } = input;
|
|
3122
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3123
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3124
|
+
if (!persisted) throw new Error(`[device-manager] setRole: unknown device id=${deviceId}`);
|
|
3125
|
+
const { meta: m } = persisted;
|
|
3126
|
+
const key = String(deviceId);
|
|
3127
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
3128
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3129
|
+
...allMeta,
|
|
3130
|
+
[key]: {
|
|
3131
|
+
...m,
|
|
3132
|
+
role
|
|
3342
3133
|
}
|
|
3134
|
+
} });
|
|
3135
|
+
});
|
|
3136
|
+
pctx.host.ctx.eventBus.emit({
|
|
3137
|
+
id: randomUUID(),
|
|
3138
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3139
|
+
source: {
|
|
3140
|
+
type: "device",
|
|
3141
|
+
id: deviceId
|
|
3142
|
+
},
|
|
3143
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3144
|
+
data: {
|
|
3145
|
+
deviceId,
|
|
3146
|
+
field: "role",
|
|
3147
|
+
value: role
|
|
3343
3148
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
for (const childId of await pctx.metaStore.directChildIds(id)) await removeCascade(childId);
|
|
3362
|
-
await removeOne(id);
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Normalize an operator-authored display override at write time so the render
|
|
3153
|
+
* path's `UNIT_TABLE` lookups always hit canonical spellings — maps `unit` and
|
|
3154
|
+
* every `perCap[*].unit` through `normalizeUnit`. Pure: rebuilds new objects,
|
|
3155
|
+
* never mutates the input. A unit `normalizeUnit` cannot canonicalize is left
|
|
3156
|
+
* as-is (the render path refuses conversion for unknown spellings).
|
|
3157
|
+
*/
|
|
3158
|
+
function normalizeDisplayOverride(display) {
|
|
3159
|
+
return {
|
|
3160
|
+
...display,
|
|
3161
|
+
...display.unit !== void 0 ? { unit: normalizeUnit(display.unit) ?? display.unit } : {},
|
|
3162
|
+
...display.perCap !== void 0 ? { perCap: Object.fromEntries(Object.entries(display.perCap).map(([cap, refine]) => [cap, refine.unit !== void 0 ? {
|
|
3163
|
+
...refine,
|
|
3164
|
+
unit: normalizeUnit(refine.unit) ?? refine.unit
|
|
3165
|
+
} : refine])) } : {}
|
|
3363
3166
|
};
|
|
3364
|
-
await removeCascade(deviceId);
|
|
3365
|
-
return { success: true };
|
|
3366
3167
|
}
|
|
3367
3168
|
/**
|
|
3368
|
-
*
|
|
3369
|
-
*
|
|
3370
|
-
*
|
|
3371
|
-
*
|
|
3372
|
-
*
|
|
3373
|
-
* `integrationId` never match.
|
|
3169
|
+
* Set (or clear) the per-device display override on a device's meta row.
|
|
3170
|
+
* Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
|
|
3171
|
+
* entirely (immutable rest-destructure — matches the "absent ⇒ no override"
|
|
3172
|
+
* projection contract, never persists `display: undefined`). Override units are
|
|
3173
|
+
* normalized at write so the render path always looks up canonical spellings.
|
|
3374
3174
|
*/
|
|
3375
|
-
async function
|
|
3376
|
-
const {
|
|
3377
|
-
const
|
|
3378
|
-
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3175
|
+
async function setDisplay(pctx, input) {
|
|
3176
|
+
const { deviceId, display } = input;
|
|
3177
|
+
const normalized = display === null ? null : normalizeDisplayOverride(display);
|
|
3178
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3179
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3180
|
+
if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
|
|
3181
|
+
const { meta: m } = persisted;
|
|
3182
|
+
const key = String(deviceId);
|
|
3183
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
3184
|
+
const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
|
|
3185
|
+
...m,
|
|
3186
|
+
display: normalized
|
|
3187
|
+
};
|
|
3188
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3189
|
+
...allMeta,
|
|
3190
|
+
[key]: nextRow
|
|
3191
|
+
} });
|
|
3381
3192
|
});
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
if (!found) return {};
|
|
3395
|
-
const storedMap = found.device.config.entries().find((e) => e.key === "_profileMap")?.value;
|
|
3396
|
-
if (storedMap !== void 0 && typeof storedMap === "object" && storedMap !== null) return storedMap;
|
|
3397
|
-
if (!isCameraDevice(found.device)) return {};
|
|
3398
|
-
const sources = await found.device.getStreamSources();
|
|
3399
|
-
const profileMap = {};
|
|
3400
|
-
for (const s of sources) if (s.profileHint && s.id) profileMap[s.profileHint] = s.id;
|
|
3401
|
-
return profileMap;
|
|
3402
|
-
}
|
|
3403
|
-
async function setStreamProfileMap(pctx, input) {
|
|
3404
|
-
const { deviceId } = input;
|
|
3405
|
-
if (pctx.registry) {
|
|
3406
|
-
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
3407
|
-
if (found) {
|
|
3408
|
-
await found.device.config.setAll({ _profileMap: input.profileMap });
|
|
3409
|
-
return { success: true };
|
|
3193
|
+
pctx.host.ctx.eventBus.emit({
|
|
3194
|
+
id: randomUUID(),
|
|
3195
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3196
|
+
source: {
|
|
3197
|
+
type: "device",
|
|
3198
|
+
id: deviceId
|
|
3199
|
+
},
|
|
3200
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3201
|
+
data: {
|
|
3202
|
+
deviceId,
|
|
3203
|
+
field: "display",
|
|
3204
|
+
value: normalized
|
|
3410
3205
|
}
|
|
3411
|
-
}
|
|
3412
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
|
|
3413
|
-
await pctx.requireDeviceOps(deviceId).setConfig({
|
|
3414
|
-
deviceId,
|
|
3415
|
-
values: { _profileMap: input.profileMap }
|
|
3416
3206
|
});
|
|
3417
|
-
return { success: true };
|
|
3418
|
-
}
|
|
3419
|
-
async function probeStreams(pctx, input) {
|
|
3420
|
-
const streamProbe = pctx.host.ctx.kernel.streamProbe;
|
|
3421
|
-
if (!streamProbe) return [];
|
|
3422
|
-
const sources = await pctx.provider.getStreamSources({ deviceId: input.deviceId });
|
|
3423
|
-
const results = [];
|
|
3424
|
-
for (const s of sources) {
|
|
3425
|
-
if (!s.url) continue;
|
|
3426
|
-
try {
|
|
3427
|
-
const metadata = await streamProbe.probe(s.url, { force: true });
|
|
3428
|
-
results.push({
|
|
3429
|
-
streamId: s.id,
|
|
3430
|
-
width: metadata.width,
|
|
3431
|
-
height: metadata.height,
|
|
3432
|
-
codec: metadata.codec,
|
|
3433
|
-
fps: metadata.fps,
|
|
3434
|
-
bitrateKbps: metadata.bitrateKbps
|
|
3435
|
-
});
|
|
3436
|
-
} catch (err) {
|
|
3437
|
-
pctx.host.ctx.logger.debug("streamProbe.probe failed — returning placeholder", { meta: {
|
|
3438
|
-
deviceId: input.deviceId,
|
|
3439
|
-
streamId: s.id,
|
|
3440
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3441
|
-
} });
|
|
3442
|
-
results.push({ streamId: s.id });
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
return results;
|
|
3446
|
-
}
|
|
3447
|
-
async function discoverDevices(pctx, input) {
|
|
3448
|
-
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
3449
|
-
if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device discovery`);
|
|
3450
|
-
return (await dp.discoverDevices({})).map((d) => ({
|
|
3451
|
-
stableId: d.stableId,
|
|
3452
|
-
type: d.type,
|
|
3453
|
-
suggestedName: d.suggestedName,
|
|
3454
|
-
prefilledConfig: d.prefilledConfig
|
|
3455
|
-
}));
|
|
3456
3207
|
}
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
} catch (err) {
|
|
3464
|
-
pctx.host.ctx.logger.warn("adoptDevice: integrationId stamp failed (device adopted)", {
|
|
3465
|
-
tags: {
|
|
3466
|
-
deviceId: summary.id,
|
|
3467
|
-
integrationId: input.integrationId
|
|
3468
|
-
},
|
|
3469
|
-
meta: { error: errMsg(err) }
|
|
3470
|
-
});
|
|
3471
|
-
}
|
|
3472
|
-
return summary;
|
|
3208
|
+
/**
|
|
3209
|
+
* Read the operator-authored per-role display defaults. Empty record when none
|
|
3210
|
+
* set. Not per-device — a plain top-level-key read.
|
|
3211
|
+
*/
|
|
3212
|
+
async function getRoleDisplayDefaults(pctx, _input) {
|
|
3213
|
+
return { defaults: (await pctx.metaStore.readStore()).roleDisplayDefaults ?? {} };
|
|
3473
3214
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3215
|
+
/**
|
|
3216
|
+
* Replace the per-role display defaults whole-record (full replace). Override
|
|
3217
|
+
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
3218
|
+
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
3219
|
+
* no interaction with the `deviceMeta` write lock. Not per-device, so no event
|
|
3220
|
+
* is emitted; the UI invalidates its own query on mutate.
|
|
3221
|
+
*/
|
|
3222
|
+
async function setRoleDisplayDefaults(pctx, input) {
|
|
3223
|
+
const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
|
|
3224
|
+
...def,
|
|
3225
|
+
unit: normalizeUnit(def.unit) ?? def.unit
|
|
3226
|
+
} : def]));
|
|
3227
|
+
await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
|
|
3478
3228
|
}
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3229
|
+
/**
|
|
3230
|
+
* Batched meta pre-seed. Applies every provided field to the
|
|
3231
|
+
* device's meta row in ONE read-modify-write under a single
|
|
3232
|
+
* `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
|
|
3233
|
+
* then emits one `DeviceMetaChanged` event per field that was
|
|
3234
|
+
* supplied — preserving the exact semantics of the individual
|
|
3235
|
+
* setters (`setName` / `setLocation` / `setType` /
|
|
3236
|
+
* `setIntegrationId` / `setLinkDeviceId` / `setRole`). Omitted
|
|
3237
|
+
* fields are left untouched; `null` clears `location` /
|
|
3238
|
+
* `linkDeviceId` / `role`. Idempotent.
|
|
3239
|
+
*
|
|
3240
|
+
* Collapses the per-child meta pre-seed (up to 6 individual
|
|
3241
|
+
* setter round-trips, each its own lock + write) into one — the
|
|
3242
|
+
* dominant cost when the kernel's `spawnAccessoryChild` reconciles
|
|
3243
|
+
* a many-entity container.
|
|
3244
|
+
*/
|
|
3245
|
+
async function applyInitialMeta(pctx, input) {
|
|
3246
|
+
const { deviceId, name, location, type, integrationId, linkDeviceId, role } = input;
|
|
3247
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3248
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3249
|
+
if (!persisted) throw new Error(`[device-manager] applyInitialMeta: unknown device id=${deviceId}`);
|
|
3250
|
+
const { meta: m } = persisted;
|
|
3251
|
+
const key = String(deviceId);
|
|
3252
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
3253
|
+
const merged = {
|
|
3254
|
+
...m,
|
|
3255
|
+
...name !== void 0 ? { name } : {},
|
|
3256
|
+
...location !== void 0 ? { location } : {},
|
|
3257
|
+
...type !== void 0 ? { type } : {},
|
|
3258
|
+
...integrationId !== void 0 ? { integrationId } : {},
|
|
3259
|
+
...linkDeviceId !== void 0 ? { linkDeviceId } : {},
|
|
3260
|
+
...role !== void 0 ? { role } : {}
|
|
3261
|
+
};
|
|
3262
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3263
|
+
...allMeta,
|
|
3264
|
+
[key]: merged
|
|
3265
|
+
} });
|
|
3485
3266
|
});
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3267
|
+
const emitMetaChanged = (field, value) => {
|
|
3268
|
+
pctx.host.ctx.eventBus.emit({
|
|
3269
|
+
id: randomUUID(),
|
|
3270
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3271
|
+
source: {
|
|
3272
|
+
type: "device",
|
|
3273
|
+
id: deviceId
|
|
3493
3274
|
},
|
|
3494
|
-
|
|
3275
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3276
|
+
data: {
|
|
3277
|
+
deviceId,
|
|
3278
|
+
field,
|
|
3279
|
+
value
|
|
3280
|
+
}
|
|
3495
3281
|
});
|
|
3496
|
-
}
|
|
3497
|
-
|
|
3282
|
+
};
|
|
3283
|
+
if (name !== void 0) emitMetaChanged("name", name);
|
|
3284
|
+
if (location !== void 0) emitMetaChanged("location", location);
|
|
3285
|
+
if (type !== void 0) emitMetaChanged("type", type);
|
|
3286
|
+
if (integrationId !== void 0) emitMetaChanged("integrationId", integrationId);
|
|
3287
|
+
if (linkDeviceId !== void 0) emitMetaChanged("linkDeviceId", linkDeviceId);
|
|
3288
|
+
if (role !== void 0) emitMetaChanged("role", role);
|
|
3498
3289
|
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3290
|
+
/**
|
|
3291
|
+
* Patch the device's hardware-identity metadata blob. Shallow
|
|
3292
|
+
* merge — `null` removes a key, anything else overwrites.
|
|
3293
|
+
* Drivers populate factual fields on first probe; operators
|
|
3294
|
+
* augment via the Device Info tab. Idempotent: a no-op patch
|
|
3295
|
+
* (every key already present with the same value) doesn't emit
|
|
3296
|
+
* the meta-changed event.
|
|
3297
|
+
*/
|
|
3298
|
+
async function setMetadata(pctx, input) {
|
|
3299
|
+
const { deviceId, patch } = input;
|
|
3300
|
+
const result = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3301
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
|
|
3302
|
+
const key = String(deviceId);
|
|
3303
|
+
const map = await pctx.metaStore.readMetadataMap();
|
|
3304
|
+
const next = { ...map[key] ?? {} };
|
|
3305
|
+
let changed = false;
|
|
3306
|
+
for (const [k, v] of Object.entries(patch)) if (v === null) {
|
|
3307
|
+
if (k in next) {
|
|
3308
|
+
delete next[k];
|
|
3309
|
+
changed = true;
|
|
3310
|
+
}
|
|
3311
|
+
} else if (next[k] !== v) {
|
|
3312
|
+
next[k] = v;
|
|
3313
|
+
changed = true;
|
|
3314
|
+
}
|
|
3315
|
+
if (!changed) return { changed: false };
|
|
3316
|
+
const hasFields = Object.keys(next).length > 0;
|
|
3317
|
+
const updatedMap = { ...map };
|
|
3318
|
+
if (hasFields) updatedMap[key] = next;
|
|
3319
|
+
else delete updatedMap[key];
|
|
3320
|
+
await pctx.settings.writeAddonStore({ deviceMetadata: updatedMap });
|
|
3321
|
+
return {
|
|
3322
|
+
changed: true,
|
|
3323
|
+
finalMeta: hasFields ? next : null
|
|
3324
|
+
};
|
|
3325
|
+
});
|
|
3326
|
+
if (!result.changed) return;
|
|
3327
|
+
pctx.host.ctx.eventBus.emit({
|
|
3328
|
+
id: randomUUID(),
|
|
3329
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3330
|
+
source: {
|
|
3331
|
+
type: "device",
|
|
3332
|
+
id: deviceId
|
|
3333
|
+
},
|
|
3334
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3335
|
+
data: {
|
|
3336
|
+
deviceId,
|
|
3337
|
+
field: "metadata",
|
|
3338
|
+
value: result.finalMeta
|
|
3339
|
+
}
|
|
3505
3340
|
});
|
|
3506
3341
|
}
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3342
|
+
/**
|
|
3343
|
+
* Soft-disable the device. Persisted on the meta row;
|
|
3344
|
+
* lifecycle gating is the driver's responsibility (BaseDevice
|
|
3345
|
+
* exposes `this.disabled` for the driver to consult at the top
|
|
3346
|
+
* of its lifecycle methods).
|
|
3347
|
+
*/
|
|
3348
|
+
async function setDisabled(pctx, input) {
|
|
3349
|
+
const { deviceId, disabled } = input;
|
|
3350
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3351
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3352
|
+
if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
|
|
3353
|
+
const { meta: m } = persisted;
|
|
3354
|
+
const key = String(deviceId);
|
|
3355
|
+
const allMeta = await pctx.metaStore.readMeta();
|
|
3356
|
+
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3357
|
+
...allMeta,
|
|
3358
|
+
[key]: {
|
|
3359
|
+
...m,
|
|
3360
|
+
disabled
|
|
3361
|
+
}
|
|
3362
|
+
} });
|
|
3363
|
+
});
|
|
3364
|
+
pctx.host.ctx.eventBus.emit({
|
|
3365
|
+
id: randomUUID(),
|
|
3366
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3367
|
+
source: {
|
|
3368
|
+
type: "device",
|
|
3369
|
+
id: deviceId
|
|
3370
|
+
},
|
|
3371
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3372
|
+
data: {
|
|
3373
|
+
deviceId,
|
|
3374
|
+
field: "disabled",
|
|
3375
|
+
value: disabled
|
|
3376
|
+
}
|
|
3377
|
+
});
|
|
3510
3378
|
}
|
|
3511
|
-
async function
|
|
3512
|
-
const {
|
|
3513
|
-
|
|
3379
|
+
async function loadRuntimeState(pctx, input) {
|
|
3380
|
+
const { deviceId } = input;
|
|
3381
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
3382
|
+
const data = await pctx.settings.readDeviceRuntimeState(deviceId);
|
|
3383
|
+
pctx.host.seedMirror(deviceId, pctx.host.withResetSessionProbe(data));
|
|
3384
|
+
return data;
|
|
3514
3385
|
}
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3386
|
+
/**
|
|
3387
|
+
* Union of (1) operator-curated location registry and (2) labels
|
|
3388
|
+
* currently in use on persisted devices. Case-insensitive
|
|
3389
|
+
* dedupe (preserves the first-seen casing). Sorted
|
|
3390
|
+
* case-insensitively for stable UI. Drives the Device Info
|
|
3391
|
+
* location autocomplete.
|
|
3392
|
+
*/
|
|
3393
|
+
async function listLocations(pctx) {
|
|
3394
|
+
const store = await pctx.settings.readAddonStore();
|
|
3395
|
+
const meta = store.deviceMeta ?? {};
|
|
3396
|
+
const locations = store.locations ?? [];
|
|
3397
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3398
|
+
const consider = (raw) => {
|
|
3399
|
+
if (typeof raw !== "string") return;
|
|
3400
|
+
const trimmed = raw.trim();
|
|
3401
|
+
if (trimmed.length === 0) return;
|
|
3402
|
+
const key = trimmed.toLowerCase();
|
|
3403
|
+
if (!seen.has(key)) seen.set(key, trimmed);
|
|
3404
|
+
};
|
|
3405
|
+
for (const label of locations) consider(label);
|
|
3406
|
+
for (const m of Object.values(meta)) consider(m.location);
|
|
3407
|
+
return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
|
|
3518
3408
|
}
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3409
|
+
/**
|
|
3410
|
+
* Add a label to the curated location registry. Idempotent:
|
|
3411
|
+
* existing entries (case-insensitive match) are silently kept.
|
|
3412
|
+
* Empty / whitespace-only inputs throw — operators must supply a
|
|
3413
|
+
* meaningful label.
|
|
3414
|
+
*/
|
|
3415
|
+
async function addLocation(pctx, input) {
|
|
3416
|
+
const trimmed = input.name.trim();
|
|
3417
|
+
if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
|
|
3418
|
+
const current = (await pctx.settings.readAddonStore()).locations ?? [];
|
|
3419
|
+
if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
|
|
3420
|
+
await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
|
|
3522
3421
|
}
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3422
|
+
/**
|
|
3423
|
+
* Remove a label from the curated registry. Match is
|
|
3424
|
+
* case-insensitive. Devices that still reference this label keep
|
|
3425
|
+
* their `meta.location` value (the registry is a suggestion
|
|
3426
|
+
* list, not a foreign key) — pass `cascade: true` to also clear
|
|
3427
|
+
* `setLocation` on every device that referenced this exact
|
|
3428
|
+
* label. Cascade only matches case-insensitively + trimmed, same
|
|
3429
|
+
* as the registry equality check.
|
|
3430
|
+
*/
|
|
3431
|
+
async function removeLocation(pctx, input) {
|
|
3432
|
+
const trimmed = input.name.trim();
|
|
3433
|
+
if (trimmed.length === 0) return;
|
|
3434
|
+
const store = await pctx.settings.readAddonStore();
|
|
3435
|
+
const current = store.locations ?? [];
|
|
3436
|
+
const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
|
|
3437
|
+
if (remaining.length !== current.length) await pctx.settings.writeAddonStore({ locations: remaining });
|
|
3438
|
+
if (input.cascade !== true) return;
|
|
3439
|
+
const meta = store.deviceMeta ?? {};
|
|
3440
|
+
const updates = { ...meta };
|
|
3441
|
+
const cleared = [];
|
|
3442
|
+
for (const [key, m] of Object.entries(meta)) {
|
|
3443
|
+
if (typeof m.location !== "string") continue;
|
|
3444
|
+
if (m.location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
|
|
3445
|
+
updates[key] = {
|
|
3446
|
+
...m,
|
|
3447
|
+
location: null
|
|
3448
|
+
};
|
|
3449
|
+
cleared.push(m.id);
|
|
3450
|
+
}
|
|
3451
|
+
if (cleared.length === 0) return;
|
|
3452
|
+
await pctx.settings.writeAddonStore({ deviceMeta: updates });
|
|
3453
|
+
for (const deviceId of cleared) pctx.host.ctx.eventBus.emit({
|
|
3454
|
+
id: randomUUID(),
|
|
3455
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3456
|
+
source: {
|
|
3457
|
+
type: "device",
|
|
3458
|
+
id: deviceId
|
|
3459
|
+
},
|
|
3460
|
+
category: EventCategory.DeviceMetaChanged,
|
|
3461
|
+
data: {
|
|
3462
|
+
deviceId,
|
|
3463
|
+
field: "location",
|
|
3464
|
+
value: null
|
|
3534
3465
|
}
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
//#endregion
|
|
3469
|
+
//#region src/builtins/device-manager/device-link-overlay.ts
|
|
3470
|
+
/** Build both lookup maps from resolved link entries. Pure — order-preserving. */
|
|
3471
|
+
function buildLinkIndexes(entries) {
|
|
3472
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3473
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
3474
|
+
for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
|
|
3475
|
+
const tKey = `${targetDeviceId}:${link.target.cap}`;
|
|
3476
|
+
const tList = targets.get(tKey) ?? [];
|
|
3477
|
+
tList.push({
|
|
3478
|
+
link,
|
|
3479
|
+
sourceDeviceId,
|
|
3480
|
+
...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
|
|
3481
|
+
});
|
|
3482
|
+
targets.set(tKey, tList);
|
|
3483
|
+
const addDependent = (sourceId, sourceCap) => {
|
|
3484
|
+
const sKey = `${sourceId}:${sourceCap}`;
|
|
3485
|
+
const sList = dependents.get(sKey) ?? [];
|
|
3486
|
+
sList.push({
|
|
3487
|
+
targetDeviceId,
|
|
3488
|
+
targetCap: link.target.cap
|
|
3489
|
+
});
|
|
3490
|
+
dependents.set(sKey, sList);
|
|
3491
|
+
};
|
|
3492
|
+
if (link.source.kind === "expression") {
|
|
3493
|
+
if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
|
|
3494
|
+
const binding = link.source.bindings[name];
|
|
3495
|
+
if (binding === void 0 || binding.kind === "literal") continue;
|
|
3496
|
+
addDependent(bindingDeviceId, binding.cap);
|
|
3497
|
+
}
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
|
|
3539
3501
|
}
|
|
3540
3502
|
return {
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
resetToSource
|
|
3544
|
-
}),
|
|
3545
|
-
removedChildren
|
|
3503
|
+
targets,
|
|
3504
|
+
dependents
|
|
3546
3505
|
};
|
|
3547
3506
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3507
|
+
//#endregion
|
|
3508
|
+
//#region src/builtins/device-manager/device-meta-store.ts
|
|
3509
|
+
/**
|
|
3510
|
+
* Meta persistence layer for the device-manager addon.
|
|
3511
|
+
*
|
|
3512
|
+
* `DeviceMetaStore` encapsulates the read/modify/write helpers that previously
|
|
3513
|
+
* lived as closures inside `onInitialize`: the `ctx.settings`-backed reads
|
|
3514
|
+
* (`readStore` / `readIndex` / `readMeta` / `readMetadataMap`), the
|
|
3515
|
+
* single-writer mutex (`withMetaWriteLock`), id allocation
|
|
3516
|
+
* (`allocateNextDeviceId`), ownership/child resolution, link-source resolution,
|
|
3517
|
+
* and the cross-device link reverse-index rebuild. Extracting it as a class
|
|
3518
|
+
* (constructed once per `onInitialize`) keeps the EXACT semantics — the
|
|
3519
|
+
* `withMetaWriteLock` promise-chain is per-instance state, identical to the
|
|
3520
|
+
* former closure — while letting the meta-action + query modules depend on a
|
|
3521
|
+
* named, injectable surface instead of captured closures.
|
|
3522
|
+
*
|
|
3523
|
+
* Cross-device link state (the `linkTargets` / `linkDependents` /
|
|
3524
|
+
* `expectedSourceStableIds` maps + the `devicesWithLinks` gate) lives on the
|
|
3525
|
+
* addon; the store reaches it through the injected `LinkIndexHost` so the
|
|
3526
|
+
* single owner of that state stays the addon instance.
|
|
3527
|
+
*/
|
|
3528
|
+
var DeviceMetaStore = class {
|
|
3529
|
+
settings;
|
|
3530
|
+
registry;
|
|
3531
|
+
linkHost;
|
|
3532
|
+
/** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
|
|
3533
|
+
* The persisted meta store is authoritative but reads are async; hub-side
|
|
3534
|
+
* callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
|
|
3535
|
+
* ownership without awaiting. Kept in sync with every register/remove and
|
|
3536
|
+
* warmed from persistence on boot. */
|
|
3537
|
+
idToAddonId = /* @__PURE__ */ new Map();
|
|
3538
|
+
/** Serialises every read-modify-write of the deviceMeta / deviceIndex blob
|
|
3539
|
+
* through one promise chain (see `withMetaWriteLock`). Per-instance state —
|
|
3540
|
+
* identical to the former `onInitialize` closure variable. */
|
|
3541
|
+
metaWriteChain = Promise.resolve();
|
|
3542
|
+
constructor(settings, registry, linkHost) {
|
|
3543
|
+
this.settings = settings;
|
|
3544
|
+
this.registry = registry;
|
|
3545
|
+
this.linkHost = linkHost;
|
|
3546
|
+
}
|
|
3547
|
+
readStore = async () => {
|
|
3548
|
+
return await this.settings.readAddonStore();
|
|
3559
3549
|
};
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
labels: [],
|
|
3563
|
-
error: void 0
|
|
3550
|
+
readIndex = async () => {
|
|
3551
|
+
return (await this.readStore()).deviceIndex ?? {};
|
|
3564
3552
|
};
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3553
|
+
readMeta = async () => {
|
|
3554
|
+
return (await this.readStore()).deviceMeta ?? {};
|
|
3555
|
+
};
|
|
3556
|
+
/** Hardware-identity metadata map. Lives in a sibling key on the
|
|
3557
|
+
* device-manager addon store so its writers (`setMetadata`) never
|
|
3558
|
+
* collide with the lifecycle writers on `deviceMeta`
|
|
3559
|
+
* (`registerDevice` / `setName` / `setLocation` / `setDisabled`).
|
|
3560
|
+
* Single-writer per row eliminates the "writer X clobbers writer
|
|
3561
|
+
* Y's field" bug class — `setMetadata` is the only producer. */
|
|
3562
|
+
readMetadataMap = async () => {
|
|
3563
|
+
return (await this.readStore()).deviceMetadata ?? {};
|
|
3564
|
+
};
|
|
3565
|
+
withMetaWriteLock = async (fn) => {
|
|
3566
|
+
const previous = this.metaWriteChain;
|
|
3567
|
+
let release = () => {};
|
|
3568
|
+
const next = new Promise((resolve) => {
|
|
3569
|
+
release = resolve;
|
|
3570
|
+
});
|
|
3571
|
+
this.metaWriteChain = next;
|
|
3572
|
+
try {
|
|
3573
|
+
await previous.catch(() => {});
|
|
3574
|
+
return await fn();
|
|
3575
|
+
} finally {
|
|
3576
|
+
release();
|
|
3577
|
+
}
|
|
3578
|
+
};
|
|
3579
|
+
/**
|
|
3580
|
+
* Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
|
|
3581
|
+
* Scans persisted meta — live IDevice lookup (hub registry) is handled
|
|
3582
|
+
* separately per call site so callers can decide whether to route to
|
|
3583
|
+
* an in-process driver or to the cross-process `device-ops` bridge.
|
|
3584
|
+
* Returns null when no device with that id is known to the hub.
|
|
3585
|
+
*/
|
|
3586
|
+
resolvePersistedById = async (deviceId) => {
|
|
3587
|
+
const m = (await this.readMeta())[String(deviceId)];
|
|
3588
|
+
if (!m) return null;
|
|
3589
|
+
return {
|
|
3590
|
+
addonId: m.addonId,
|
|
3591
|
+
stableId: m.stableId,
|
|
3592
|
+
meta: m
|
|
3593
|
+
};
|
|
3594
|
+
};
|
|
3595
|
+
/** Direct children of a device: the union of the live registry's children
|
|
3596
|
+
* and the persisted-meta scan (`parentDeviceId === parentId`), deduplicated
|
|
3597
|
+
* and excluding self. Shared by the `remove` cascade and the `resetToSource`
|
|
3598
|
+
* resync purge (#19). */
|
|
3599
|
+
directChildIds = async (parentId) => {
|
|
3600
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3601
|
+
if (this.registry) for (const c of this.registry.getChildren(parentId)) ids.add(c.id);
|
|
3602
|
+
const meta = await this.readMeta();
|
|
3603
|
+
for (const m of Object.values(meta)) if (m.parentDeviceId === parentId) ids.add(m.id);
|
|
3604
|
+
ids.delete(parentId);
|
|
3605
|
+
return [...ids];
|
|
3606
|
+
};
|
|
3607
|
+
/** Resolve a link's `sourceKey` to a live device id: the sibling accessory
|
|
3608
|
+
* whose stableId is `${parentStableId}-${sourceKey}`. Null when absent.
|
|
3609
|
+
* Pure over a pre-read meta map so a multi-link resolve reads the store once. */
|
|
3610
|
+
resolveSourceDeviceId = (parentStableId, sourceKey, meta) => {
|
|
3611
|
+
const wanted = `${parentStableId}-${sourceKey}`;
|
|
3612
|
+
for (const m of Object.values(meta)) if (m.stableId === wanted) return m.id;
|
|
3613
|
+
return null;
|
|
3614
|
+
};
|
|
3615
|
+
/** Resolve a GLOBAL link source (P2e) to a live device id: the device whose
|
|
3616
|
+
* FULL stableId equals `sourceStableId`, regardless of parent container.
|
|
3617
|
+
* First match wins (stableIds are effectively unique cluster-wide — see the
|
|
3618
|
+
* `DeviceLinkGlobalSource` docblock). Null when absent. Pure over a
|
|
3619
|
+
* pre-read meta map so a multi-link resolve reads the store once. */
|
|
3620
|
+
resolveGlobalSourceDeviceId = (sourceStableId, meta) => {
|
|
3621
|
+
for (const m of Object.values(meta)) if (m.stableId === sourceStableId) return m.id;
|
|
3622
|
+
return null;
|
|
3623
|
+
};
|
|
3624
|
+
allocateNextDeviceId = async () => {
|
|
3625
|
+
const current = (await this.readStore()).nextDeviceId ?? 1;
|
|
3626
|
+
await this.settings.writeAddonStore({ nextDeviceId: current + 1 });
|
|
3627
|
+
return current;
|
|
3628
|
+
};
|
|
3629
|
+
/** Rebuild the `linkTargets` / `linkDependents` reverse-index maps from the
|
|
3630
|
+
* current persisted meta. Called at boot (once the `devicesWithLinks` seed
|
|
3631
|
+
* has run) and whenever the link topology can change: `setDeviceLinks`,
|
|
3632
|
+
* `registerDevice` (a new sibling source may resolve previously-dangling
|
|
3633
|
+
* links), and `removeDevice` (its entries must be evicted). */
|
|
3634
|
+
rebuildLinkDependents = async () => {
|
|
3635
|
+
if (this.linkHost.devicesWithLinks.size === 0) {
|
|
3636
|
+
this.linkHost.setLinkIndexes(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const allMeta = await this.readMeta();
|
|
3640
|
+
const stableIdById = /* @__PURE__ */ new Map();
|
|
3641
|
+
const idByStableId = /* @__PURE__ */ new Map();
|
|
3642
|
+
for (const m of Object.values(allMeta)) {
|
|
3643
|
+
stableIdById.set(m.id, m.stableId);
|
|
3644
|
+
idByStableId.set(m.stableId, m.id);
|
|
3645
|
+
}
|
|
3646
|
+
const entries = [];
|
|
3647
|
+
const expectedSources = /* @__PURE__ */ new Set();
|
|
3648
|
+
for (const targetId of this.linkHost.devicesWithLinks) {
|
|
3649
|
+
const targetMeta = Object.values(allMeta).find((m) => m.id === targetId);
|
|
3650
|
+
if (!targetMeta) continue;
|
|
3651
|
+
const container = targetMeta.parentDeviceId !== null ? stableIdById.get(targetMeta.parentDeviceId) ?? stableIdById.get(targetId) : stableIdById.get(targetId);
|
|
3652
|
+
if (container === void 0) continue;
|
|
3653
|
+
for (const link of targetMeta.deviceLinks ?? []) {
|
|
3654
|
+
const src = link.source;
|
|
3655
|
+
if (src.kind === "expression") {
|
|
3656
|
+
const bindingSourceIds = {};
|
|
3657
|
+
for (const [name, b] of Object.entries(src.bindings)) {
|
|
3658
|
+
if (b.kind === "literal") continue;
|
|
3659
|
+
const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
|
|
3660
|
+
expectedSources.add(wanted);
|
|
3661
|
+
const bid = idByStableId.get(wanted);
|
|
3662
|
+
if (bid !== void 0) bindingSourceIds[name] = bid;
|
|
3663
|
+
}
|
|
3664
|
+
entries.push({
|
|
3665
|
+
targetDeviceId: targetId,
|
|
3666
|
+
link,
|
|
3667
|
+
sourceDeviceId: -1,
|
|
3668
|
+
bindingSourceIds
|
|
3669
|
+
});
|
|
3670
|
+
continue;
|
|
3671
|
+
}
|
|
3672
|
+
if (src.kind === "literal") {
|
|
3673
|
+
entries.push({
|
|
3674
|
+
targetDeviceId: targetId,
|
|
3675
|
+
link,
|
|
3676
|
+
sourceDeviceId: -1
|
|
3677
|
+
});
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3680
|
+
const wantedStableId = src.kind === "global" ? src.sourceStableId : `${container}-${src.sourceKey}`;
|
|
3681
|
+
expectedSources.add(wantedStableId);
|
|
3682
|
+
const srcId = idByStableId.get(wantedStableId);
|
|
3683
|
+
if (srcId === void 0) continue;
|
|
3684
|
+
entries.push({
|
|
3685
|
+
targetDeviceId: targetId,
|
|
3686
|
+
link,
|
|
3687
|
+
sourceDeviceId: srcId
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
const { targets, dependents } = buildLinkIndexes(entries);
|
|
3692
|
+
this.linkHost.setLinkIndexes(targets, dependents, expectedSources);
|
|
3693
|
+
};
|
|
3694
|
+
};
|
|
3571
3695
|
//#endregion
|
|
3572
3696
|
//#region src/builtins/device-manager/device-link-resolver.ts
|
|
3573
3697
|
/** Returns true when `x` is a non-null, non-array plain object. */
|
|
@@ -4538,6 +4662,10 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
4538
4662
|
listAll: (input) => listAll(pctx, input),
|
|
4539
4663
|
getDevice: (input) => getDevice(pctx, input),
|
|
4540
4664
|
getChildren: (input) => getChildren(pctx, input),
|
|
4665
|
+
getLinkedDevices: (input) => getLinkedDevices(pctx, input),
|
|
4666
|
+
getDeviceSettingsContribution: (input) => buildLinkedDevicesContribution(pctx, input.deviceId),
|
|
4667
|
+
getDeviceLiveContribution: async () => null,
|
|
4668
|
+
applyDeviceSettingsPatch: (input) => applyLinkedDevicesPatch(pctx, input.deviceId, input.patch),
|
|
4541
4669
|
getStreamSources: (input) => getStreamSources(pctx, input),
|
|
4542
4670
|
getConfigSchema: (input) => getConfigSchema(pctx, input),
|
|
4543
4671
|
getSettingsSchema: (input) => getSettingsSchema(pctx, input),
|