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