@camstack/system 1.2.486 → 1.2.488

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.
Files changed (86) hide show
  1. package/dist/{agent-https-DP-Ds2hU.js → agent-https-0EEnqeUL.js} +1 -1
  2. package/dist/{agent-https-C4PLR74Z.mjs → agent-https-C24nO-la.mjs} +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/calendars/calendars.addon.js +1 -1
  14. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  15. package/dist/builtins/camera-grid/index.js +1 -1
  16. package/dist/builtins/camera-grid/index.mjs +1 -1
  17. package/dist/builtins/composer/actuator-dispatch.d.ts +13 -0
  18. package/dist/builtins/composer/actuator-groups.d.ts +54 -0
  19. package/dist/builtins/composer/actuator-invoke.d.ts +11 -0
  20. package/dist/builtins/composer/composed-device.d.ts +13 -24
  21. package/dist/builtins/composer/composer.addon.js +408 -8
  22. package/dist/builtins/composer/composer.addon.mjs +408 -8
  23. package/dist/builtins/composer/composer.d.ts +11 -1
  24. package/dist/builtins/console-logging/index.js +1 -1
  25. package/dist/builtins/console-logging/index.mjs +1 -1
  26. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  27. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  28. package/dist/builtins/device-history/device-history.addon.js +2 -2
  29. package/dist/builtins/device-history/device-history.addon.mjs +2 -2
  30. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  31. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  32. package/dist/builtins/hub-forwarder/index.js +1 -1
  33. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  34. package/dist/builtins/keychain/index.js +3 -3
  35. package/dist/builtins/keychain/index.mjs +3 -3
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  37. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  38. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  39. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  40. package/dist/builtins/local-network/local-network.addon.js +2 -2
  41. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  42. package/dist/builtins/loki-logging/index.js +1 -1
  43. package/dist/builtins/loki-logging/index.mjs +1 -1
  44. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  45. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  46. package/dist/builtins/platform-probe/index.js +1 -1
  47. package/dist/builtins/platform-probe/index.mjs +1 -1
  48. package/dist/builtins/record-entries/index.js +1 -1
  49. package/dist/builtins/record-entries/index.mjs +1 -1
  50. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  51. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/snapshot/index.js +1 -1
  53. package/dist/builtins/snapshot/index.mjs +1 -1
  54. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  55. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/index.js +1 -1
  57. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  60. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/system-config/system-config.addon.js +3 -3
  63. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  64. package/dist/builtins/winston-logging/index.js +1 -1
  65. package/dist/builtins/winston-logging/index.mjs +1 -1
  66. package/dist/{composition-sources-IxiUkrz1.mjs → composition-sources-BA2e5vh_.mjs} +1 -1
  67. package/dist/{composition-sources-CN8K2Q91.js → composition-sources-BulDAgW2.js} +1 -1
  68. package/dist/{constants-1S7BBGMb.js → constants-BntdWx4k.js} +1 -1
  69. package/dist/{constants-DLyGFHZ2.mjs → constants-DZKo4kM2.mjs} +1 -1
  70. package/dist/{device-ca-DoOFG0nm.js → device-ca-BuRg4V4x.js} +1 -1
  71. package/dist/{device-ca-Cbf7_R9t.mjs → device-ca-DXm8IH3p.mjs} +1 -1
  72. package/dist/{dist-D-d_UWL4.js → dist-CrsVjBbk.js} +375 -15
  73. package/dist/{dist-HBue971z.mjs → dist-Dfwxsc_Y.mjs} +310 -16
  74. package/dist/file-host-worker.js +1 -1
  75. package/dist/file-host-worker.mjs +1 -1
  76. package/dist/{hub-mdns-ciao-CuEWb6gB.mjs → hub-mdns-ciao-BbI8MNtQ.mjs} +2 -2
  77. package/dist/{hub-mdns-ciao-dcacrwqd.js → hub-mdns-ciao-CLroVKhP.js} +2 -2
  78. package/dist/index.js +7 -7
  79. package/dist/index.mjs +7 -7
  80. package/dist/{restore-archive-DHdXrKwG.js → restore-archive-CPoja7N3.js} +1 -1
  81. package/dist/{restore-archive-BGBRadFJ.mjs → restore-archive-CSsOMQuX.mjs} +1 -1
  82. package/dist/{retention-ChgrkTfz.js → retention-DgA4qLBj.js} +1 -1
  83. package/dist/{retention-8uqDWll8.mjs → retention-Dz5t6cWO.mjs} +1 -1
  84. package/dist/{wal-maintenance-DubGwqz2.mjs → wal-maintenance-BJi3_DBN.mjs} +0 -0
  85. package/dist/{wal-maintenance-BOT7wWvH.js → wal-maintenance-CL9hrbnn.js} +0 -0
  86. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { $n as planComposition, Br as COMPOSER_ADDON_ID, Gi as createEvent, Gn as motionCapability, Gr as compositionFeatureEnabled, Hr as cloneStatefulExpressionMemory, It as composerCapability, Jr as compositionSourceKey, Kr as compositionItemFieldPath, Lt as compositionFieldKey, N as DeclaredDevices, Ot as buildCapabilityLookup, Ur as composedDeviceRef, Wr as composedDeviceStableId, Xr as disarmRoseMemory, Yr as createStatefulExpressionMemory, Zr as toExpressionValue, a as BaseDevice, cn as evaluateCompositionField, d as COMPOSED_ITEM_FIXED_FIELDS, dn as expressionContainsBuiltinCall, ei as DeviceType, ja as EventCategory, ni as BaseAddon, qr as compositionSliceKey, r as ALL_CAPABILITY_DEFINITIONS, ti as errMsg } from "../../dist-HBue971z.mjs";
2
- import { n as normalizedNativeOwner } from "../../composition-sources-IxiUkrz1.mjs";
3
- import { i as DAY_MS, m as capSeriesKey, t as DEFAULT_RETENTION } from "../../retention-8uqDWll8.mjs";
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. */
@@ -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
- * It serves NO command method: slice 1 refuses any cap with one
31
- * (`has-commands`), and commands arrive with `forward` in slice 3. So each
32
- * composed cap is registered with an EMPTY native provider, and `getStatus`
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,6 +81,13 @@ 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
+ }
48
91
  function isRecord$2(value) {
49
92
  return typeof value === "object" && value !== null && !Array.isArray(value);
50
93
  }
@@ -57,7 +100,11 @@ function sliceBoolean(slice, field) {
57
100
  throw new Error(`composed ${field} is unknown — not a stand-in value`);
58
101
  }
59
102
  var ComposedDevice = class extends BaseDevice {
60
- features = [];
103
+ sharedFeatures = [];
104
+ extraCaps = /* @__PURE__ */ new Set();
105
+ get features() {
106
+ return this.sharedFeatures;
107
+ }
61
108
  constructor(ctx, type, caps) {
62
109
  super(ctx, COMPOSED_DEVICE_CONFIG_SCHEMA, { type });
63
110
  for (const cap of caps) {
@@ -66,10 +113,80 @@ var ComposedDevice = class extends BaseDevice {
66
113
  ctx.registerNativeCap(motionCapability, { isDetected: async () => sliceBoolean(read(), "detected") });
67
114
  continue;
68
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
+ }
69
122
  ctx.registerNativeCap(cap, {});
70
123
  }
124
+ if (type === DeviceType.Switch || type === DeviceType.Light) ctx.registerNativeCap(groupCommandsCapability, {});
71
125
  this.markOnline(false);
72
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
+ }
73
190
  seed(capName, slice) {
74
191
  try {
75
192
  this.runtimeState.setCapState(capName, { ...slice });
@@ -239,6 +356,223 @@ var ClaimGate = class {
239
356
  }
240
357
  };
241
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
242
576
  //#region src/builtins/composer/composed-device-set.ts
243
577
  /**
244
578
  * The composed devices the composer DECLARED (D659): one row per `new`-target
@@ -3270,6 +3604,7 @@ var Composer = class {
3270
3604
  claimGate;
3271
3605
  targets;
3272
3606
  grafts;
3607
+ actuators;
3273
3608
  /** The rows the composer owns, read ONCE before its first withdrawal decision. */
3274
3609
  rowSeed;
3275
3610
  /**
@@ -3301,6 +3636,7 @@ var Composer = class {
3301
3636
  this.grafts.describing(deviceId);
3302
3637
  return deps.describeTarget(deviceId);
3303
3638
  });
3639
+ this.actuators = this.attachActuators(deps);
3304
3640
  this.waits = new ApplyWaits(deps.logger);
3305
3641
  this.rowSeed = composedRowSeed({
3306
3642
  logger: deps.logger,
@@ -3352,9 +3688,33 @@ var Composer = class {
3352
3688
  this.targets.invalidate(data.deviceId);
3353
3689
  this.grafts.onBindingsChanged(data);
3354
3690
  this.dispatch(await this.deps.tracker.onBindingsChanged(data.deviceId, data.capName, data.reason));
3691
+ await this.chained(() => this.actuators?.onMemberBindings(data.deviceId));
3355
3692
  }
3356
3693
  shutdown() {
3357
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
+ });
3358
3718
  }
3359
3719
  /** Run `work` after every apply already queued, whether that settled or failed. */
3360
3720
  chained(work) {
@@ -3388,6 +3748,10 @@ var Composer = class {
3388
3748
  await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
3389
3749
  const unreleasedClaims = await this.releaseConfirmed(releasing);
3390
3750
  await this.syncSources(rebuilt);
3751
+ await this.actuators?.sync(input.blocks.map((block) => ({
3752
+ id: block.id,
3753
+ helper: block.composition.helper
3754
+ })));
3391
3755
  await this.reconcileClaims();
3392
3756
  return {
3393
3757
  blocks: planned.map((p) => this.verdict(p.block.id)),
@@ -4819,7 +5183,43 @@ var ComposerAddon = class extends BaseAddon {
4819
5183
  describeTarget: readBoundCaps,
4820
5184
  grafts,
4821
5185
  now: () => Date.now(),
4822
- 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)
4823
5223
  });
4824
5224
  this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
4825
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;
@@ -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-D-d_UWL4.js");
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 { Ln as logDestinationCapability, ni as BaseAddon } from "../../dist-HBue971z.mjs";
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 = {
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-D-d_UWL4.js");
3
+ const require_dist = require("../../dist-CrsVjBbk.js");
4
4
  const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
5
- const require_composition_sources = require("../../composition-sources-CN8K2Q91.js");
5
+ const require_composition_sources = require("../../composition-sources-BulDAgW2.js");
6
6
  const require_prefix_range = require("../../prefix-range-D_KueXbQ.js");
7
7
  const require_prefix_walk = require("../../prefix-walk-Dv6MLZlM.js");
8
8
  let zod = require("zod");
@@ -1,6 +1,6 @@
1
- import { $n as planComposition, Bt as coreBlockComposition, Ht as coreBlockSourceKind, Ji as defineRecordKinds, Jr as compositionSourceKey, Kn as normalizeCoreBlockWrite, N as DeclaredDevices, Oa as transformSecretWrite, Ot as buildCapabilityLookup, Pr as validateComposition, Rt as compositionSourceRefs, Ui as collectSecretMarkerRefs, Ur as composedDeviceRef, Ut as coreBlocksCapability, Vt as coreBlockIdFromAddonId, Zt as describeComposableCaps, dr as resolveCoreBlockFiles, f as COMPOSER_RUNNER_ID, la as isSecretRefOwnedBy, m as CORE_BLOCK_ADDON_PREFIX, ni as BaseAddon, p as CORE_BLOCKS_ADDON_ID, r as ALL_CAPABILITY_DEFINITIONS, sa as isSecretMarker, sr as replacedFieldKeys, ta as hydrateSchema, ti as errMsg, tr as problemsBlockSave, ur as resolveCoreBlockEntry, v as CoreBlockSchema, wa as secretTreeFromSchema, xa as resolveSecretMarkers, zt as coreBlockAddonId } from "../../dist-HBue971z.mjs";
1
+ import { At as buildCapabilityLookup, Fa as secretTreeFromSchema, Gr as validateComposition, Gt as coreBlockSourceKind, Ht as coreBlockAddonId, Kt as coreBlocksCapability, Ma as resolveSecretMarkers, N as DeclaredDevices, Qn as normalizeCoreBlockWrite, Ut as coreBlockComposition, Vt as compositionSourceRefs, Wt as coreBlockIdFromAddonId, _r as resolveCoreBlockEntry, aa as defineRecordKinds, ai as compositionSourceKey, ba as isSecretRefOwnedBy, cr as problemsBlockSave, ea as collectSecretMarkerRefs, ei as composedDeviceRef, en as describeComposableCaps, f as COMPOSER_RUNNER_ID, fa as hydrateSchema, fi as errMsg, m as CORE_BLOCK_ADDON_PREFIX, mr as replacedFieldKeys, or as planComposition, p as CORE_BLOCKS_ADDON_ID, pi as BaseAddon, r as ALL_CAPABILITY_DEFINITIONS, v as CoreBlockSchema, va as isSecretMarker, vr as resolveCoreBlockFiles, za as transformSecretWrite } from "../../dist-Dfwxsc_Y.mjs";
2
2
  import { t as require_read_all_rows } from "../../read-all-rows-TaPqdWNF.mjs";
3
- import { t as describeCompositionSources } from "../../composition-sources-IxiUkrz1.mjs";
3
+ import { t as describeCompositionSources } from "../../composition-sources-BA2e5vh_.mjs";
4
4
  import { t as prefixRange } from "../../prefix-range-Dl0znowN.mjs";
5
5
  import { n as unwrapStoredValue, t as loadPrefixed } from "../../prefix-walk-DAOBSyH1.mjs";
6
6
  import { createRequire } from "node:module";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D-d_UWL4.js");
7
- const require_retention = require("../../retention-ChgrkTfz.js");
6
+ const require_dist = require("../../dist-CrsVjBbk.js");
7
+ const require_retention = require("../../retention-DgA4qLBj.js");
8
8
  //#region src/builtins/device-history/capture/point-writer.ts
9
9
  var FLUSH_INTERVAL_MS = 1e4;
10
10
  var MAX_BUFFER_AGE_MS = 6e4;
@@ -1,5 +1,5 @@
1
- import { B as HISTORY_ITEM_CATALOG, Ct as armRemovalReconcile, D as DEVICE_HISTORY_ENERGY_KEY, Xi as emitReadiness, aa as isReadingQuantity, ea as historyUnitFor, en as deviceHistoryCapability, ja as EventCategory, ni as BaseAddon, nn as deviceRemovalHookCapability, z as HISTORY_CATALOG } from "../../dist-HBue971z.mjs";
2
- import { S as subjectIdOf, _ as readUnit, a as FIVE_MINUTES_MS, b as SeriesRegistry, c as bucketEndOf, d as CAP_KEY_PREFIX, f as CaptureEngine, g as readPath, h as catalogMetrics, i as DAY_MS, l as bucketStartOf, m as capSeriesKey, n as RetentionJob, o as HOUR_MS, p as capItemSeriesKey, r as DepartureTracker, s as ROLLUP_GRACE_MS, t as DEFAULT_RETENTION, u as floorTo, v as DeviceDirectory, x as identityOf, y as isRealDeviceType } from "../../retention-8uqDWll8.mjs";
1
+ import { B as HISTORY_CATALOG, D as DEVICE_HISTORY_ENERGY_KEY, Ha as EventCategory, V as HISTORY_ITEM_CATALOG, an as deviceRemovalHookCapability, da as historyUnitFor, ga as isReadingQuantity, pi as BaseAddon, rn as deviceHistoryCapability, sa as emitReadiness, wt as armRemovalReconcile } from "../../dist-Dfwxsc_Y.mjs";
2
+ import { S as subjectIdOf, _ as readUnit, a as FIVE_MINUTES_MS, b as SeriesRegistry, c as bucketEndOf, d as CAP_KEY_PREFIX, f as CaptureEngine, g as readPath, h as catalogMetrics, i as DAY_MS, l as bucketStartOf, m as capSeriesKey, n as RetentionJob, o as HOUR_MS, p as capItemSeriesKey, r as DepartureTracker, s as ROLLUP_GRACE_MS, t as DEFAULT_RETENTION, u as floorTo, v as DeviceDirectory, x as identityOf, y as isRealDeviceType } from "../../retention-Dz5t6cWO.mjs";
3
3
  //#region src/builtins/device-history/capture/point-writer.ts
4
4
  var FLUSH_INTERVAL_MS = 1e4;
5
5
  var MAX_BUFFER_AGE_MS = 6e4;
@@ -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-D-d_UWL4.js");
6
+ const require_dist = require("../../dist-CrsVjBbk.js");
7
7
  const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
8
8
  const require_prefix_range = require("../../prefix-range-D_KueXbQ.js");
9
9
  const require_prefix_walk = require("../../prefix-walk-Dv6MLZlM.js");
@@ -1,4 +1,4 @@
1
- import { $r as DeviceRole, $t as deviceExtensionCapability, Ca as secretTreeFromPaths, Da as toSecretFieldView, F as DeviceTombstoneSchema, In as locationIconKey, Ji as defineRecordKinds, L as FieldClaimModeSchema, Mn as legacyAutoLinkIds, Oa as transformSecretWrite, P as DeviceStatusSchema, Pi as WELL_KNOWN_TAB_MAP, Pt as canonicalIconKey, Qr as DeviceFeature, R as FieldClaimSchema, Sr as siteZoneKey, Ta as sleep$1, Wi as collectZodSecretPaths, Wt as createDeviceExtensionProvider, Y as MINUTES_PER_DAY, Yt as defineExtensionSlotProvider, Zn as parseStreamParamsFormPatch, ca as isSecretMarkerLike, ei as DeviceType, gn as formatMinutes, gr as runtimeStatePolicyFor, ia as isKeychainStoredField, in as deviceStatusCapability, ja as EventCategory, jt as buildStreamParamsConfigSchema, ka as userError, ki as SECRET_CLEAR, l as CAP_NAMES_WITH_STATUS, na as isDeviceConfigCap, ni as BaseAddon, oa as isSecretClear, on as enumerateItemArrayFields, ot as STREAM_PROFILE_META, qn as normalizeUnit, r as ALL_CAPABILITY_DEFINITIONS, rn as deviceStateCapability, s as CAMERA_SWITCH_ORDER, sa as isSecretMarker, sn as enumerateSchemaFields, ti as errMsg, tn as deviceManagerCapability, un as expandContainer, w as DAY_NAMES, xa as resolveSecretMarkers } from "../../dist-HBue971z.mjs";
1
+ import { $n as normalizeUnit, Ba as userError, Bi as SECRET_CLEAR, F as DeviceTombstoneSchema, Gi as WELL_KNOWN_TAB_MAP, Ha as EventCategory, Hn as locationIconKey, Ia as sleep$1, It as canonicalIconKey, L as FieldClaimModeSchema, Ma as resolveSecretMarkers, Mr as siteZoneKey, Nt as buildStreamParamsConfigSchema, P as DeviceStatusSchema, Pa as secretTreeFromPaths, Qt as defineExtensionSlotProvider, R as FieldClaimSchema, Ra as toSecretFieldView, Rn as legacyAutoLinkIds, X as MINUTES_PER_DAY, _a as isSecretClear, aa as defineRecordKinds, bn as formatMinutes, di as DeviceType, fi as errMsg, ha as isKeychainStoredField, in as deviceManagerCapability, ir as parseStreamParamsFormPatch, l as CAP_NAMES_WITH_STATUS, li as DeviceFeature, ln as enumerateItemArrayFields, nn as deviceExtensionCapability, on as deviceStateCapability, pa as isDeviceConfigCap, pi as BaseAddon, pn as expandContainer, qt as createDeviceExtensionProvider, r as ALL_CAPABILITY_DEFINITIONS, s as CAMERA_SWITCH_ORDER, sn as deviceStatusCapability, st as STREAM_PROFILE_META, ta as collectZodSecretPaths, ui as DeviceRole, un as enumerateSchemaFields, va as isSecretMarker, w as DAY_NAMES, wr as runtimeStatePolicyFor, ya as isSecretMarkerLike, za as transformSecretWrite } from "../../dist-Dfwxsc_Y.mjs";
2
2
  import { t as require_read_all_rows } from "../../read-all-rows-TaPqdWNF.mjs";
3
3
  import { t as prefixRange } from "../../prefix-range-Dl0znowN.mjs";
4
4
  import { t as loadPrefixed } from "../../prefix-walk-DAOBSyH1.mjs";
@@ -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-D-d_UWL4.js");
6
+ const require_dist = require("../../dist-CrsVjBbk.js");
7
7
  const require_formatter = require("../../formatter-xbxeV4GD.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { Ln as logDestinationCapability, ni as BaseAddon } from "../../dist-HBue971z.mjs";
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/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -3,10 +3,10 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D-d_UWL4.js");
7
- const require_device_ca = require("../../device-ca-DoOFG0nm.js");
6
+ const require_dist = require("../../dist-CrsVjBbk.js");
7
+ const require_device_ca = require("../../device-ca-BuRg4V4x.js");
8
8
  const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
9
- const require_constants = require("../../constants-1S7BBGMb.js");
9
+ const require_constants = require("../../constants-BntdWx4k.js");
10
10
  const require_platform_key_source = require("../../platform-key-source-Di7HrlGR.js");
11
11
  let node_crypto = require("node:crypto");
12
12
  let _camstack_types_node = require("@camstack/types/node");