@camstack/system 1.2.485 → 1.2.487
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/{agent-https-DBP7P-ee.js → agent-https-0EEnqeUL.js} +1 -1
- package/dist/{agent-https-_EXvi6F8.mjs → agent-https-C24nO-la.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/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/actuator-dispatch.d.ts +13 -0
- package/dist/builtins/composer/actuator-groups.d.ts +54 -0
- package/dist/builtins/composer/actuator-invoke.d.ts +11 -0
- package/dist/builtins/composer/composed-device.d.ts +13 -24
- package/dist/builtins/composer/composer.addon.js +464 -18
- package/dist/builtins/composer/composer.addon.mjs +464 -18
- package/dist/builtins/composer/composer.d.ts +11 -1
- package/dist/builtins/composer/composition-runtime.d.ts +2 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/composition-sources.d.ts +3 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +53 -28
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +53 -28
- package/dist/builtins/device-history/device-history.addon.js +2 -2
- package/dist/builtins/device-history/device-history.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/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/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- 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 +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +284 -284
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BnZpIA8u.mjs → composition-sources-BA2e5vh_.mjs} +4 -2
- package/dist/{composition-sources-BSQneZV9.js → composition-sources-BulDAgW2.js} +4 -2
- package/dist/{constants-CQfTeCnB.js → constants-BntdWx4k.js} +1 -1
- package/dist/{constants-7D7xjw0h.mjs → constants-DZKo4kM2.mjs} +1 -1
- package/dist/{device-ca-D_4dI5gv.js → device-ca-BuRg4V4x.js} +1 -1
- package/dist/{device-ca-Bzd6UO_c.mjs → device-ca-DXm8IH3p.mjs} +1 -1
- package/dist/{dist-xCNJAQMh.js → dist-CrsVjBbk.js} +1127 -26
- package/dist/{dist-Df6c0RX_.mjs → dist-Dfwxsc_Y.mjs} +1062 -27
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-D7GMnEz0.mjs → hub-mdns-ciao-BbI8MNtQ.mjs} +2 -2
- package/dist/{hub-mdns-ciao-DRfmLR_Z.js → hub-mdns-ciao-CLroVKhP.js} +2 -2
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/{restore-archive-Co70oRya.mjs → restore-archive-BB5QxUIe.mjs} +82 -82
- package/dist/{restore-archive-DA7N6ZZA.js → restore-archive-CPoja7N3.js} +1 -1
- package/dist/{retention-DNDOPW7Z.js → retention-DgA4qLBj.js} +1 -1
- package/dist/{retention-DfCW-dZS.mjs → retention-Dz5t6cWO.mjs} +1 -1
- package/dist/{wal-maintenance-71-j48Ci.mjs → wal-maintenance-BJi3_DBN.mjs} +0 -0
- package/dist/{wal-maintenance-CA0L6WZ9.js → wal-maintenance-CL9hrbnn.js} +0 -0
- package/package.json +1 -1
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { $
|
|
2
|
-
import { n as normalizedNativeOwner } from "../../composition-sources-
|
|
3
|
-
import { i as DAY_MS, m as capSeriesKey, t as DEFAULT_RETENTION } from "../../retention-
|
|
1
|
+
import { $r as cloneStatefulExpressionMemory, Ar as sharedLightCaps, At as buildCapabilityLookup, Bt as compositionFieldKey, Cn as groupTemplate, Cr as runFanOut, Ha as EventCategory, N as DeclaredDevices, Rt as colorCapability, Sn as groupCommandsCapability, Ur as switchCapability, Zn as motionCapability, Zr as COMPOSER_ADDON_ID, a as BaseDevice, ai as compositionSourceKey, ci as toExpressionValue, d as COMPOSED_ITEM_FIXED_FIELDS, di as DeviceType, dn as evaluateCompositionField, ei as composedDeviceRef, fi as errMsg, gn as fanOutTargets, ii as compositionSliceKey, kr as sharedColorModes, kt as brightnessCapability, mn as expressionContainsBuiltinCall, na as createEvent, ni as compositionFeatureEnabled, oi as createStatefulExpressionMemory, or as planComposition, pi as BaseAddon, r as ALL_CAPABILITY_DEFINITIONS, ri as compositionItemFieldPath, rr as parseColorInput, si as disarmRoseMemory, ti as composedDeviceStableId, z as GroupCommandResultSchema, zt as composerCapability } from "../../dist-Dfwxsc_Y.mjs";
|
|
2
|
+
import { n as normalizedNativeOwner } from "../../composition-sources-BA2e5vh_.mjs";
|
|
3
|
+
import { i as DAY_MS, m as capSeriesKey, t as DEFAULT_RETENTION } from "../../retention-Dz5t6cWO.mjs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region src/builtins/shared/device-state-changed.ts
|
|
6
6
|
/** Structural narrowing helper for untrusted bus payloads. */
|
|
7
|
-
function isRecord$
|
|
7
|
+
function isRecord$3(value) {
|
|
8
8
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
9
|
}
|
|
10
10
|
function parseDeviceStateChanged(data) {
|
|
11
|
-
if (!isRecord$
|
|
11
|
+
if (!isRecord$3(data)) return null;
|
|
12
12
|
const deviceId = data["deviceId"];
|
|
13
13
|
const capName = data["capName"];
|
|
14
14
|
const slice = data["slice"];
|
|
15
15
|
if (typeof deviceId !== "number" || typeof capName !== "string") return null;
|
|
16
|
-
if (!isRecord$
|
|
16
|
+
if (!isRecord$3(slice)) return null;
|
|
17
17
|
return {
|
|
18
18
|
deviceId,
|
|
19
19
|
capName,
|
|
@@ -21,15 +21,51 @@ function parseDeviceStateChanged(data) {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
|
+
//#region src/builtins/composer/actuator-invoke.ts
|
|
25
|
+
/**
|
|
26
|
+
* Turn one fan-out call into the member's existing cap method. A bad argument
|
|
27
|
+
* fails that member only — the caller still attempts the rest.
|
|
28
|
+
*/
|
|
29
|
+
async function invokeActuatorCall(api, call) {
|
|
30
|
+
if (call.cap === "switch" && call.method === "setState") {
|
|
31
|
+
const on = call.args.on;
|
|
32
|
+
if (typeof on !== "boolean") throw new Error("switch.setState needs a boolean on");
|
|
33
|
+
await api.switchSetState(call.deviceId, on);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (call.cap === "brightness" && call.method === "setBrightness") {
|
|
37
|
+
const percentage = call.args.percentage;
|
|
38
|
+
if (typeof percentage !== "number" || !Number.isFinite(percentage)) throw new Error("brightness.setBrightness needs a finite percentage");
|
|
39
|
+
await api.setBrightness(call.deviceId, percentage);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (call.cap === "color" && call.method === "setColor") {
|
|
43
|
+
const color = parseColorInput(call.args.color);
|
|
44
|
+
if (color === null) throw new Error("color.setColor needs a colour mode");
|
|
45
|
+
await api.setColor(call.deviceId, color);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`no fan-out for ${call.cap}.${call.method}`);
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/builtins/composer/actuator-dispatch.ts
|
|
52
|
+
var dispatcher = null;
|
|
53
|
+
function setActuatorDispatcher(next) {
|
|
54
|
+
dispatcher = next;
|
|
55
|
+
}
|
|
56
|
+
function actuatorDispatcher() {
|
|
57
|
+
return dispatcher;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
24
60
|
//#region src/builtins/composer/composed-device.ts
|
|
25
61
|
/**
|
|
26
62
|
* A composed device: an ordinary `BaseDevice` whose caps are served by the
|
|
27
63
|
* composer (D659). Every consumer (exporters, NC, UI, HomeKit) reads it
|
|
28
64
|
* through its bindings, unchanged.
|
|
29
65
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
66
|
+
* A reading group serves no command. An actuator helper's `switch` (and, on
|
|
67
|
+
* a light, `brightness` / `color` when every member binds them) fans the
|
|
68
|
+
* call out through `actuatorDispatcher` (D944). `getStatus`
|
|
33
69
|
* (R4) is the kernel's: `registerNativeCap` installs a default that reads the
|
|
34
70
|
* device's runtime slice for any cap declaring both `status` and
|
|
35
71
|
* `runtimeState` (`device-cap-proxy.ts`, "Default `getStatus`"), on the
|
|
@@ -45,13 +81,112 @@ function parseDeviceStateChanged(data) {
|
|
|
45
81
|
* declaration is the authority for what this device is.
|
|
46
82
|
*/
|
|
47
83
|
var COMPOSED_DEVICE_CONFIG_SCHEMA = z.object({});
|
|
84
|
+
function priorResults(slice) {
|
|
85
|
+
if (!isRecord$2(slice) || !Array.isArray(slice.results)) return [];
|
|
86
|
+
return slice.results.flatMap((row) => {
|
|
87
|
+
const parsed = GroupCommandResultSchema.safeParse(row);
|
|
88
|
+
return parsed.success ? [parsed.data] : [];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function isRecord$2(value) {
|
|
92
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
/** Unknown is a throw, never `false` (D393). */
|
|
95
|
+
function sliceBoolean(slice, field) {
|
|
96
|
+
if (isRecord$2(slice)) {
|
|
97
|
+
const value = slice[field];
|
|
98
|
+
if (typeof value === "boolean") return value;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`composed ${field} is unknown — not a stand-in value`);
|
|
101
|
+
}
|
|
48
102
|
var ComposedDevice = class extends BaseDevice {
|
|
49
|
-
|
|
103
|
+
sharedFeatures = [];
|
|
104
|
+
extraCaps = /* @__PURE__ */ new Set();
|
|
105
|
+
get features() {
|
|
106
|
+
return this.sharedFeatures;
|
|
107
|
+
}
|
|
50
108
|
constructor(ctx, type, caps) {
|
|
51
109
|
super(ctx, COMPOSED_DEVICE_CONFIG_SCHEMA, { type });
|
|
52
|
-
for (const cap of caps)
|
|
110
|
+
for (const cap of caps) {
|
|
111
|
+
if (cap.name === motionCapability.name) {
|
|
112
|
+
const read = () => this.runtimeState.getCapState(motionCapability.name);
|
|
113
|
+
ctx.registerNativeCap(motionCapability, { isDetected: async () => sliceBoolean(read(), "detected") });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (cap.name === switchCapability.name) {
|
|
117
|
+
ctx.registerNativeCap(switchCapability, { setState: async (input) => {
|
|
118
|
+
await this.dispatch("switch", "setState", { on: input.on });
|
|
119
|
+
} });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
ctx.registerNativeCap(cap, {});
|
|
123
|
+
}
|
|
124
|
+
if (type === DeviceType.Switch || type === DeviceType.Light) ctx.registerNativeCap(groupCommandsCapability, {});
|
|
53
125
|
this.markOnline(false);
|
|
54
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Register or drop brightness and colour, and publish the shared colour
|
|
129
|
+
* modes. Switch stays: it is the active state, registered with the device.
|
|
130
|
+
*/
|
|
131
|
+
syncShared(caps, modes) {
|
|
132
|
+
this.ensureExtra(brightnessCapability.name, caps.includes(brightnessCapability.name), () => {
|
|
133
|
+
this.ctx.registerNativeCap(brightnessCapability, { setBrightness: async (input) => {
|
|
134
|
+
await this.dispatch("brightness", "setBrightness", { percentage: input.percentage });
|
|
135
|
+
} });
|
|
136
|
+
});
|
|
137
|
+
this.ensureExtra(colorCapability.name, caps.includes(colorCapability.name), () => {
|
|
138
|
+
this.ctx.registerNativeCap(colorCapability, { setColor: async (input) => {
|
|
139
|
+
await this.dispatch("color", "setColor", { color: input.color });
|
|
140
|
+
} });
|
|
141
|
+
});
|
|
142
|
+
this.sharedFeatures = [...modes];
|
|
143
|
+
this.refreshFeatures();
|
|
144
|
+
}
|
|
145
|
+
/** Keep each member's latest attempt. A later command replaces that member only. */
|
|
146
|
+
recordCommands(incoming) {
|
|
147
|
+
const prior = priorResults(this.runtimeState.getCapState(groupCommandsCapability.name));
|
|
148
|
+
const byDevice = new Map(prior.map((row) => [row.deviceId, row]));
|
|
149
|
+
for (const row of incoming) byDevice.set(row.deviceId, row);
|
|
150
|
+
try {
|
|
151
|
+
this.runtimeState.setCapState(groupCommandsCapability.name, { results: [...byDevice.values()] });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this.ctx.logger.error("actuator group could not store command results", {
|
|
154
|
+
tags: { deviceId: this.id },
|
|
155
|
+
meta: { error: errMsg(err) }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
ensureExtra(capName, want, register) {
|
|
160
|
+
const has = this.extraCaps.has(capName);
|
|
161
|
+
if (want && !has) {
|
|
162
|
+
register();
|
|
163
|
+
this.extraCaps = new Set([...this.extraCaps, capName]);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!want && has) {
|
|
167
|
+
this.ctx.unregisterNativeCap?.(capName);
|
|
168
|
+
this.extraCaps = new Set([...this.extraCaps].filter((name) => name !== capName));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async dispatch(cap, method, args) {
|
|
172
|
+
const run = actuatorDispatcher();
|
|
173
|
+
if (run === null) {
|
|
174
|
+
this.ctx.logger.warn("actuator group command was not sent — fan-out is not attached", {
|
|
175
|
+
tags: { deviceId: this.id },
|
|
176
|
+
meta: {
|
|
177
|
+
cap,
|
|
178
|
+
method
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
throw new Error("actuator fan-out is not attached");
|
|
182
|
+
}
|
|
183
|
+
await run({
|
|
184
|
+
groupDeviceId: this.id,
|
|
185
|
+
cap,
|
|
186
|
+
method,
|
|
187
|
+
args
|
|
188
|
+
});
|
|
189
|
+
}
|
|
55
190
|
seed(capName, slice) {
|
|
56
191
|
try {
|
|
57
192
|
this.runtimeState.setCapState(capName, { ...slice });
|
|
@@ -221,6 +356,223 @@ var ClaimGate = class {
|
|
|
221
356
|
}
|
|
222
357
|
};
|
|
223
358
|
//#endregion
|
|
359
|
+
//#region src/builtins/composer/actuator-groups.ts
|
|
360
|
+
/**
|
|
361
|
+
* Live actuator helpers (D944). State is the aggregate the composer already
|
|
362
|
+
* runs. Commands are fanned out here: every member that binds the cap,
|
|
363
|
+
* including one that is offline, and a failure on one does not cancel the rest.
|
|
364
|
+
* A light's brightness and colour are registered only while every member binds
|
|
365
|
+
* them, and recomputed when a member's bindings change.
|
|
366
|
+
*/
|
|
367
|
+
function isActuatorGroupDevice(device) {
|
|
368
|
+
return "syncShared" in device && "recordCommands" in device && "id" in device;
|
|
369
|
+
}
|
|
370
|
+
var ActuatorGroups = class {
|
|
371
|
+
deps;
|
|
372
|
+
groups = /* @__PURE__ */ new Map();
|
|
373
|
+
bound;
|
|
374
|
+
constructor(deps) {
|
|
375
|
+
this.deps = deps;
|
|
376
|
+
this.bound = (input) => this.command(input);
|
|
377
|
+
setActuatorDispatcher(this.bound);
|
|
378
|
+
}
|
|
379
|
+
shutdown() {
|
|
380
|
+
setActuatorDispatcher(null);
|
|
381
|
+
}
|
|
382
|
+
async sync(blocks) {
|
|
383
|
+
const next = /* @__PURE__ */ new Map();
|
|
384
|
+
for (const block of blocks) {
|
|
385
|
+
const remembered = await this.remember(block);
|
|
386
|
+
if (remembered !== null) next.set(block.id, remembered);
|
|
387
|
+
}
|
|
388
|
+
this.groups = next;
|
|
389
|
+
}
|
|
390
|
+
async onMemberBindings(deviceId) {
|
|
391
|
+
const hit = [...this.groups.values()].filter((group) => group.fanOut.some((member) => member.deviceId === deviceId));
|
|
392
|
+
if (hit.length === 0) return;
|
|
393
|
+
const next = new Map(this.groups);
|
|
394
|
+
for (const group of hit) {
|
|
395
|
+
const block = {
|
|
396
|
+
id: group.blockId,
|
|
397
|
+
helper: {
|
|
398
|
+
kind: "group",
|
|
399
|
+
templateId: group.templateId,
|
|
400
|
+
aggregate: "any",
|
|
401
|
+
active: "at-least-one",
|
|
402
|
+
members: group.members.map(copyMember)
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const remembered = await this.remember(block);
|
|
406
|
+
if (remembered !== null) next.set(group.blockId, remembered);
|
|
407
|
+
}
|
|
408
|
+
this.groups = next;
|
|
409
|
+
}
|
|
410
|
+
async remember(block) {
|
|
411
|
+
const spec = block.helper;
|
|
412
|
+
if (spec === void 0) return null;
|
|
413
|
+
const template = groupTemplate(spec.templateId);
|
|
414
|
+
if (template === null || template.family !== "actuator") return null;
|
|
415
|
+
if (template.id !== "switch" && template.id !== "light") return null;
|
|
416
|
+
const device = this.deps.deviceOf(block.id);
|
|
417
|
+
if (device === null) return null;
|
|
418
|
+
const viewed = await this.viewMembers(spec.members, device.id);
|
|
419
|
+
const shared = sharedOf(template.id, viewed);
|
|
420
|
+
const previous = this.groups.get(block.id)?.shared;
|
|
421
|
+
if (template.id === "light" && previous !== void 0) this.logCapChange(device.id, previous, shared, viewed);
|
|
422
|
+
const modes = template.id === "light" && shared.includes("color") ? sharedColorModes(viewed.featureLists) : [];
|
|
423
|
+
device.syncShared(shared, modes);
|
|
424
|
+
return {
|
|
425
|
+
blockId: block.id,
|
|
426
|
+
templateId: template.id,
|
|
427
|
+
groupDeviceId: device.id,
|
|
428
|
+
members: spec.members.map(copyMember),
|
|
429
|
+
fanOut: viewed.fanOut,
|
|
430
|
+
shared
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async viewMembers(members, groupDeviceId) {
|
|
434
|
+
const fanOut = [];
|
|
435
|
+
const capLists = [];
|
|
436
|
+
const featureLists = [];
|
|
437
|
+
let complete = true;
|
|
438
|
+
for (const member of members) {
|
|
439
|
+
const resolved = this.deps.resolve(member.source);
|
|
440
|
+
if (resolved.kind !== "found") {
|
|
441
|
+
complete = false;
|
|
442
|
+
this.deps.logger.warn("actuator group left a member out of shared commands — its device was not resolved", {
|
|
443
|
+
tags: { deviceId: groupDeviceId },
|
|
444
|
+
meta: { source: `${member.source.addonId}/${member.source.stableId}` }
|
|
445
|
+
});
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const read = await this.deps.readMember(resolved.deviceId);
|
|
449
|
+
if (read.kind !== "ready") {
|
|
450
|
+
complete = false;
|
|
451
|
+
fanOut.push({
|
|
452
|
+
deviceId: resolved.deviceId,
|
|
453
|
+
caps: [],
|
|
454
|
+
online: true
|
|
455
|
+
});
|
|
456
|
+
this.deps.logger.warn("actuator group left a member out of shared commands — its capabilities could not be read", {
|
|
457
|
+
tags: { deviceId: resolved.deviceId },
|
|
458
|
+
meta: {
|
|
459
|
+
groupDeviceId,
|
|
460
|
+
error: read.error
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
capLists.push(read.caps);
|
|
466
|
+
featureLists.push(read.features);
|
|
467
|
+
fanOut.push({
|
|
468
|
+
deviceId: resolved.deviceId,
|
|
469
|
+
caps: read.caps,
|
|
470
|
+
online: true
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
fanOut,
|
|
475
|
+
capLists: complete ? capLists : null,
|
|
476
|
+
featureLists
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
logCapChange(groupDeviceId, previous, shared, viewed) {
|
|
480
|
+
for (const cap of ["brightness", "color"]) {
|
|
481
|
+
const had = previous.includes(cap);
|
|
482
|
+
const has = shared.includes(cap);
|
|
483
|
+
if (had === has) continue;
|
|
484
|
+
if (has) {
|
|
485
|
+
this.deps.logger.info("light group gained a shared capability", {
|
|
486
|
+
tags: { deviceId: groupDeviceId },
|
|
487
|
+
meta: { cap }
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
this.deps.logger.warn("light group dropped a capability a member does not bind", {
|
|
492
|
+
tags: { deviceId: groupDeviceId },
|
|
493
|
+
meta: { cap }
|
|
494
|
+
});
|
|
495
|
+
for (const member of viewed.fanOut) {
|
|
496
|
+
if (member.caps.includes(cap)) continue;
|
|
497
|
+
this.deps.logger.warn("light group dropped a capability this member does not bind", {
|
|
498
|
+
tags: { deviceId: member.deviceId },
|
|
499
|
+
meta: {
|
|
500
|
+
groupDeviceId,
|
|
501
|
+
cap
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async command(input) {
|
|
508
|
+
const group = [...this.groups.values()].find((item) => item.groupDeviceId === input.groupDeviceId);
|
|
509
|
+
if (group === void 0) {
|
|
510
|
+
this.deps.logger.warn("actuator command dropped — this device is not a live helper group", {
|
|
511
|
+
tags: { deviceId: input.groupDeviceId },
|
|
512
|
+
meta: {
|
|
513
|
+
cap: input.cap,
|
|
514
|
+
method: input.method
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
throw new Error("actuator group is not live");
|
|
518
|
+
}
|
|
519
|
+
if (!group.shared.includes(input.cap)) {
|
|
520
|
+
this.deps.logger.warn("actuator command dropped — the group does not share that capability", {
|
|
521
|
+
tags: { deviceId: input.groupDeviceId },
|
|
522
|
+
meta: {
|
|
523
|
+
cap: input.cap,
|
|
524
|
+
method: input.method
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
throw new Error(`group does not share ${input.cap}`);
|
|
528
|
+
}
|
|
529
|
+
const results = await runFanOut(fanOutTargets(group.fanOut, input.cap, input.method, input.args), (call) => this.deps.invoke(call));
|
|
530
|
+
const failed = results.flatMap((result) => result.ok ? [] : [result]);
|
|
531
|
+
for (const result of failed) this.deps.logger.warn("actuator group command failed on a member — the others were still attempted", {
|
|
532
|
+
tags: { deviceId: result.deviceId },
|
|
533
|
+
meta: {
|
|
534
|
+
groupDeviceId: input.groupDeviceId,
|
|
535
|
+
cap: input.cap,
|
|
536
|
+
method: input.method,
|
|
537
|
+
error: result.error
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
if (failed.length > 0) this.deps.logger.warn("actuator group command finished with member failures", {
|
|
541
|
+
tags: { deviceId: input.groupDeviceId },
|
|
542
|
+
meta: {
|
|
543
|
+
failed: failed.length,
|
|
544
|
+
attempted: results.length,
|
|
545
|
+
cap: input.cap
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
const at = this.deps.now();
|
|
549
|
+
this.deps.deviceOf(group.blockId)?.recordCommands(results.map((result) => ({
|
|
550
|
+
deviceId: result.deviceId,
|
|
551
|
+
cap: input.cap,
|
|
552
|
+
method: input.method,
|
|
553
|
+
ok: result.ok,
|
|
554
|
+
...result.ok ? {} : { error: result.error },
|
|
555
|
+
at
|
|
556
|
+
})));
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
function sharedOf(templateId, viewed) {
|
|
560
|
+
if (templateId === "switch") return ["switch"];
|
|
561
|
+
if (viewed.capLists === null) return ["switch"];
|
|
562
|
+
const shared = sharedLightCaps(viewed.capLists);
|
|
563
|
+
return shared.includes("switch") ? shared : ["switch", ...shared];
|
|
564
|
+
}
|
|
565
|
+
function copyMember(member) {
|
|
566
|
+
return {
|
|
567
|
+
source: {
|
|
568
|
+
addonId: member.source.addonId,
|
|
569
|
+
stableId: member.source.stableId
|
|
570
|
+
},
|
|
571
|
+
cap: member.cap,
|
|
572
|
+
fieldPath: member.fieldPath
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
224
576
|
//#region src/builtins/composer/composed-device-set.ts
|
|
225
577
|
/**
|
|
226
578
|
* The composed devices the composer DECLARED (D659): one row per `new`-target
|
|
@@ -1290,7 +1642,9 @@ function writable(record) {
|
|
|
1290
1642
|
switch (record.plan.unavailable) {
|
|
1291
1643
|
case "null": return null;
|
|
1292
1644
|
case "unknown": return "unknown";
|
|
1293
|
-
|
|
1645
|
+
case "omit":
|
|
1646
|
+
case "offline":
|
|
1647
|
+
case "release": return;
|
|
1294
1648
|
}
|
|
1295
1649
|
}
|
|
1296
1650
|
function errorText(error) {
|
|
@@ -2013,7 +2367,16 @@ var CompositionRuntime = class {
|
|
|
2013
2367
|
this.seeded = new Set([...this.seeded, cap]);
|
|
2014
2368
|
this.markWritten(cap, entries);
|
|
2015
2369
|
}
|
|
2370
|
+
/** An omitted optional reading must leave the slice, not keep the previous number (D944). */
|
|
2371
|
+
clearsOmitted(cap) {
|
|
2372
|
+
return this.recordsOf(cap).some((record) => record.plan.unavailable === "omit" && record.outcome?.state === "unavailable");
|
|
2373
|
+
}
|
|
2016
2374
|
patchCap(cap, touched) {
|
|
2375
|
+
if (this.clearsOmitted(cap)) {
|
|
2376
|
+
const entries = this.entriesFor(cap, this.recordsOf(cap));
|
|
2377
|
+
if (this.write(cap, "seed", entries)) this.markWritten(cap, entries);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2017
2380
|
const entries = this.entriesFor(cap, this.recordsOf(cap).filter((r) => touched.has(fieldKey(r.plan)) && (!r.hasWritten || writable(r) !== r.written)));
|
|
2018
2381
|
if (entries.length === 0) return;
|
|
2019
2382
|
if (this.write(cap, "patch", entries)) this.markWritten(cap, entries);
|
|
@@ -3241,6 +3604,7 @@ var Composer = class {
|
|
|
3241
3604
|
claimGate;
|
|
3242
3605
|
targets;
|
|
3243
3606
|
grafts;
|
|
3607
|
+
actuators;
|
|
3244
3608
|
/** The rows the composer owns, read ONCE before its first withdrawal decision. */
|
|
3245
3609
|
rowSeed;
|
|
3246
3610
|
/**
|
|
@@ -3272,6 +3636,7 @@ var Composer = class {
|
|
|
3272
3636
|
this.grafts.describing(deviceId);
|
|
3273
3637
|
return deps.describeTarget(deviceId);
|
|
3274
3638
|
});
|
|
3639
|
+
this.actuators = this.attachActuators(deps);
|
|
3275
3640
|
this.waits = new ApplyWaits(deps.logger);
|
|
3276
3641
|
this.rowSeed = composedRowSeed({
|
|
3277
3642
|
logger: deps.logger,
|
|
@@ -3323,9 +3688,33 @@ var Composer = class {
|
|
|
3323
3688
|
this.targets.invalidate(data.deviceId);
|
|
3324
3689
|
this.grafts.onBindingsChanged(data);
|
|
3325
3690
|
this.dispatch(await this.deps.tracker.onBindingsChanged(data.deviceId, data.capName, data.reason));
|
|
3691
|
+
await this.chained(() => this.actuators?.onMemberBindings(data.deviceId));
|
|
3326
3692
|
}
|
|
3327
3693
|
shutdown() {
|
|
3328
3694
|
for (const entry of this.live.values()) entry.runtime?.stop();
|
|
3695
|
+
this.actuators?.shutdown();
|
|
3696
|
+
}
|
|
3697
|
+
attachActuators(deps) {
|
|
3698
|
+
const read = deps.readActuatorMember;
|
|
3699
|
+
const invoke = deps.invokeActuator;
|
|
3700
|
+
if (read === void 0 || invoke === void 0) return null;
|
|
3701
|
+
return new ActuatorGroups({
|
|
3702
|
+
logger: deps.logger.child("actuators"),
|
|
3703
|
+
now: deps.now,
|
|
3704
|
+
resolve: (ref) => {
|
|
3705
|
+
const found = deps.tracker.resolution(ref);
|
|
3706
|
+
return found.kind === "found" ? {
|
|
3707
|
+
kind: "found",
|
|
3708
|
+
deviceId: found.deviceId
|
|
3709
|
+
} : { kind: "other" };
|
|
3710
|
+
},
|
|
3711
|
+
readMember: read,
|
|
3712
|
+
deviceOf: (blockId) => {
|
|
3713
|
+
const device = this.composed.get(composedDeviceStableId(blockId));
|
|
3714
|
+
return device !== null && isActuatorGroupDevice(device) ? device : null;
|
|
3715
|
+
},
|
|
3716
|
+
invoke
|
|
3717
|
+
});
|
|
3329
3718
|
}
|
|
3330
3719
|
/** Run `work` after every apply already queued, whether that settled or failed. */
|
|
3331
3720
|
chained(work) {
|
|
@@ -3359,6 +3748,10 @@ var Composer = class {
|
|
|
3359
3748
|
await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
|
|
3360
3749
|
const unreleasedClaims = await this.releaseConfirmed(releasing);
|
|
3361
3750
|
await this.syncSources(rebuilt);
|
|
3751
|
+
await this.actuators?.sync(input.blocks.map((block) => ({
|
|
3752
|
+
id: block.id,
|
|
3753
|
+
helper: block.composition.helper
|
|
3754
|
+
})));
|
|
3362
3755
|
await this.reconcileClaims();
|
|
3363
3756
|
return {
|
|
3364
3757
|
blocks: planned.map((p) => this.verdict(p.block.id)),
|
|
@@ -3741,8 +4134,21 @@ var NodeDeadlineTimer = class {
|
|
|
3741
4134
|
function unanswered(def) {
|
|
3742
4135
|
return Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => name);
|
|
3743
4136
|
}
|
|
3744
|
-
function
|
|
4137
|
+
function isSliceRecord(value) {
|
|
4138
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4139
|
+
}
|
|
4140
|
+
/** A slice field that is present. Unknown throws — it is not false or 0 (D393). */
|
|
4141
|
+
function sliceField(slice, field) {
|
|
4142
|
+
const value = slice?.[field];
|
|
4143
|
+
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
|
|
4144
|
+
throw new Error(`composed ${field} is unknown — not a stand-in value`);
|
|
4145
|
+
}
|
|
4146
|
+
function composedMethod(answer, readSlice) {
|
|
3745
4147
|
if (answer.kind === "answer") return async () => answer.value;
|
|
4148
|
+
if (answer.kind === "slice") {
|
|
4149
|
+
const field = answer.field;
|
|
4150
|
+
return async () => sliceField(await readSlice(), field);
|
|
4151
|
+
}
|
|
3746
4152
|
return async () => {
|
|
3747
4153
|
throw new Error(answer.reason);
|
|
3748
4154
|
};
|
|
@@ -3860,7 +4266,11 @@ var GraftHost = class {
|
|
|
3860
4266
|
return ctx;
|
|
3861
4267
|
}
|
|
3862
4268
|
providerFor(deviceId, def) {
|
|
3863
|
-
const
|
|
4269
|
+
const readSlice = async () => {
|
|
4270
|
+
const slice = await this.readMerged(deviceId, def.name);
|
|
4271
|
+
return isSliceRecord(slice) ? slice : null;
|
|
4272
|
+
};
|
|
4273
|
+
const answers = Object.entries(def.composedMethods ?? {}).map(([name, answer]) => [name, composedMethod(answer, readSlice)]);
|
|
3864
4274
|
const withStatus = def.status === void 0 && def.runtimeState === void 0 ? [] : [["getStatus", async () => this.readMerged(deviceId, def.name)]];
|
|
3865
4275
|
return Object.fromEntries([...answers, ...withStatus]);
|
|
3866
4276
|
}
|
|
@@ -3937,7 +4347,7 @@ function entriesOf(request) {
|
|
|
3937
4347
|
}))];
|
|
3938
4348
|
}
|
|
3939
4349
|
function valueAtPath(slice, path) {
|
|
3940
|
-
return path.split(".").reduce((node, segment) => isRecord$
|
|
4350
|
+
return path.split(".").reduce((node, segment) => isRecord$3(node) ? node[segment] : void 0, slice);
|
|
3941
4351
|
}
|
|
3942
4352
|
function onlineOf(slice) {
|
|
3943
4353
|
const online = slice["online"];
|
|
@@ -4604,12 +5014,12 @@ var DECLARED_INTEGRATION_NAME_UNUSED = "Blocks";
|
|
|
4604
5014
|
var PULSE_HISTORY_MAX_POINTS = 5e3;
|
|
4605
5015
|
/** The owner-only shadow payload is not in the typed catalog: narrowed at the boundary, like `device.state-changed`. */
|
|
4606
5016
|
function parseNativeShadowChanged(data) {
|
|
4607
|
-
if (!isRecord$
|
|
5017
|
+
if (!isRecord$3(data)) return null;
|
|
4608
5018
|
const deviceId = data["deviceId"];
|
|
4609
5019
|
const capName = data["capName"];
|
|
4610
5020
|
const native = data["native"];
|
|
4611
5021
|
if (typeof deviceId !== "number" || typeof capName !== "string") return null;
|
|
4612
|
-
if (!isRecord$
|
|
5022
|
+
if (!isRecord$3(native)) return null;
|
|
4613
5023
|
return {
|
|
4614
5024
|
deviceId,
|
|
4615
5025
|
capName,
|
|
@@ -4773,7 +5183,43 @@ var ComposerAddon = class extends BaseAddon {
|
|
|
4773
5183
|
describeTarget: readBoundCaps,
|
|
4774
5184
|
grafts,
|
|
4775
5185
|
now: () => Date.now(),
|
|
4776
|
-
newTimer: () => new NodeDeadlineTimer(() => Date.now())
|
|
5186
|
+
newTimer: () => new NodeDeadlineTimer(() => Date.now()),
|
|
5187
|
+
readActuatorMember: async (deviceId) => {
|
|
5188
|
+
try {
|
|
5189
|
+
const bindings = await api.deviceManager.getBindings.query({ deviceId });
|
|
5190
|
+
const found = await api.deviceManager.getDevice.query({ deviceId });
|
|
5191
|
+
return {
|
|
5192
|
+
kind: "ready",
|
|
5193
|
+
caps: bindings.entries.map((entry) => entry.capName),
|
|
5194
|
+
features: found === null ? [] : [...found.features]
|
|
5195
|
+
};
|
|
5196
|
+
} catch (err) {
|
|
5197
|
+
return {
|
|
5198
|
+
kind: "unreadable",
|
|
5199
|
+
error: errMsg(err)
|
|
5200
|
+
};
|
|
5201
|
+
}
|
|
5202
|
+
},
|
|
5203
|
+
invokeActuator: (call) => invokeActuatorCall({
|
|
5204
|
+
switchSetState: async (deviceId, on) => {
|
|
5205
|
+
await api.switch.setState.mutate({
|
|
5206
|
+
deviceId,
|
|
5207
|
+
on
|
|
5208
|
+
});
|
|
5209
|
+
},
|
|
5210
|
+
setBrightness: async (deviceId, percentage) => {
|
|
5211
|
+
await api.brightness.setBrightness.mutate({
|
|
5212
|
+
deviceId,
|
|
5213
|
+
percentage
|
|
5214
|
+
});
|
|
5215
|
+
},
|
|
5216
|
+
setColor: async (deviceId, color) => {
|
|
5217
|
+
await api.color.setColor.mutate({
|
|
5218
|
+
deviceId,
|
|
5219
|
+
color
|
|
5220
|
+
});
|
|
5221
|
+
}
|
|
5222
|
+
}, call)
|
|
4777
5223
|
});
|
|
4778
5224
|
this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
|
|
4779
5225
|
const data = parseDeviceStateChanged(event.data);
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
* (where a runtime writes and how it reads) and `composer-live-set.ts` (the
|
|
48
48
|
* pure reads of the live set: tracked sources, reset keys, wanted claims).
|
|
49
49
|
*/
|
|
50
|
-
import { type CapabilityLookup, type ComposerApplyInput, type ComposerApplyResult, type CompositionBlockState, type IScopedLogger } from "@camstack/types";
|
|
50
|
+
import { type CapabilityLookup, type ComposerApplyInput, type ComposerApplyResult, type FanOutCall, type CompositionBlockState, type IScopedLogger } from "@camstack/types";
|
|
51
51
|
import type { DeviceStateChangedData } from "../shared/device-state-changed.js";
|
|
52
|
+
import { type ActuatorMemberLookup } from "./actuator-groups.js";
|
|
52
53
|
import { type ComposerDeclare, type ComposerDevicePorts } from "./composed-device-set.js";
|
|
53
54
|
import { type PersistedComposedRow } from "./composition-declarations.js";
|
|
54
55
|
import type { CompositionEventEmitter, DeadlineTimer } from "./composition-runtime.js";
|
|
@@ -96,6 +97,13 @@ export interface ComposerDeps {
|
|
|
96
97
|
readonly grafts?: ComposerGraftPorts;
|
|
97
98
|
readonly now: () => number;
|
|
98
99
|
readonly newTimer: () => DeadlineTimer;
|
|
100
|
+
/**
|
|
101
|
+
* Member capabilities and the command that reaches one member. Absent in
|
|
102
|
+
* tests that predate actuator groups: those compositions still aggregate,
|
|
103
|
+
* and a command names that fan-out was not attached.
|
|
104
|
+
*/
|
|
105
|
+
readonly readActuatorMember?: (deviceId: number) => Promise<ActuatorMemberLookup>;
|
|
106
|
+
readonly invokeActuator?: (call: FanOutCall) => Promise<void>;
|
|
99
107
|
}
|
|
100
108
|
export declare class Composer {
|
|
101
109
|
private readonly deps;
|
|
@@ -108,6 +116,7 @@ export declare class Composer {
|
|
|
108
116
|
private readonly claimGate;
|
|
109
117
|
private readonly targets;
|
|
110
118
|
private readonly grafts;
|
|
119
|
+
private readonly actuators;
|
|
111
120
|
/** The rows the composer owns, read ONCE before its first withdrawal decision. */
|
|
112
121
|
private readonly rowSeed;
|
|
113
122
|
/**
|
|
@@ -134,6 +143,7 @@ export declare class Composer {
|
|
|
134
143
|
onDeviceUnregistered(deviceId: number): Promise<void>;
|
|
135
144
|
onBindingsChanged(data: DeviceBindingsChangedData): Promise<void>;
|
|
136
145
|
shutdown(): void;
|
|
146
|
+
private attachActuators;
|
|
137
147
|
/** Run `work` after every apply already queued, whether that settled or failed. */
|
|
138
148
|
private chained;
|
|
139
149
|
private applyNow;
|
|
@@ -156,6 +156,8 @@ export declare class CompositionRuntime {
|
|
|
156
156
|
/** One info line per field per direction, never one per pass. */
|
|
157
157
|
private logReleaseTransitions;
|
|
158
158
|
private seedCap;
|
|
159
|
+
/** An omitted optional reading must leave the slice, not keep the previous number (D944). */
|
|
160
|
+
private clearsOmitted;
|
|
159
161
|
private patchCap;
|
|
160
162
|
/**
|
|
161
163
|
* After a failed write nothing is known about what landed, and after a
|
|
@@ -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-CrsVjBbk.js");
|
|
7
7
|
const require_formatter = require("../../formatter-xbxeV4GD.js");
|
|
8
8
|
//#region src/builtins/console-logging/console-destination.ts
|
|
9
9
|
var LEVEL_RANK = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Un as logDestinationCapability, pi as BaseAddon } from "../../dist-Dfwxsc_Y.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-DkYZ4LXp.mjs";
|
|
3
3
|
//#region src/builtins/console-logging/console-destination.ts
|
|
4
4
|
var LEVEL_RANK = {
|