@camstack/system 1.2.486 → 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.
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 +284 -284
  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-BGBRadFJ.mjs → restore-archive-BB5QxUIe.mjs} +82 -82
  81. package/dist/{restore-archive-DHdXrKwG.js → restore-archive-CPoja7N3.js} +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
@@ -3,9 +3,9 @@ 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_composition_sources = require("../../composition-sources-CN8K2Q91.js");
8
- const require_retention = require("../../retention-ChgrkTfz.js");
6
+ const require_dist = require("../../dist-CrsVjBbk.js");
7
+ const require_composition_sources = require("../../composition-sources-BulDAgW2.js");
8
+ const require_retention = require("../../retention-DgA4qLBj.js");
9
9
  let zod = require("zod");
10
10
  //#region src/builtins/shared/device-state-changed.ts
11
11
  /** Structural narrowing helper for untrusted bus payloads. */
@@ -26,15 +26,51 @@ function parseDeviceStateChanged(data) {
26
26
  };
27
27
  }
28
28
  //#endregion
29
+ //#region src/builtins/composer/actuator-invoke.ts
30
+ /**
31
+ * Turn one fan-out call into the member's existing cap method. A bad argument
32
+ * fails that member only — the caller still attempts the rest.
33
+ */
34
+ async function invokeActuatorCall(api, call) {
35
+ if (call.cap === "switch" && call.method === "setState") {
36
+ const on = call.args.on;
37
+ if (typeof on !== "boolean") throw new Error("switch.setState needs a boolean on");
38
+ await api.switchSetState(call.deviceId, on);
39
+ return;
40
+ }
41
+ if (call.cap === "brightness" && call.method === "setBrightness") {
42
+ const percentage = call.args.percentage;
43
+ if (typeof percentage !== "number" || !Number.isFinite(percentage)) throw new Error("brightness.setBrightness needs a finite percentage");
44
+ await api.setBrightness(call.deviceId, percentage);
45
+ return;
46
+ }
47
+ if (call.cap === "color" && call.method === "setColor") {
48
+ const color = require_dist.parseColorInput(call.args.color);
49
+ if (color === null) throw new Error("color.setColor needs a colour mode");
50
+ await api.setColor(call.deviceId, color);
51
+ return;
52
+ }
53
+ throw new Error(`no fan-out for ${call.cap}.${call.method}`);
54
+ }
55
+ //#endregion
56
+ //#region src/builtins/composer/actuator-dispatch.ts
57
+ var dispatcher = null;
58
+ function setActuatorDispatcher(next) {
59
+ dispatcher = next;
60
+ }
61
+ function actuatorDispatcher() {
62
+ return dispatcher;
63
+ }
64
+ //#endregion
29
65
  //#region src/builtins/composer/composed-device.ts
30
66
  /**
31
67
  * A composed device: an ordinary `BaseDevice` whose caps are served by the
32
68
  * composer (D659). Every consumer (exporters, NC, UI, HomeKit) reads it
33
69
  * through its bindings, unchanged.
34
70
  *
35
- * It serves NO command method: slice 1 refuses any cap with one
36
- * (`has-commands`), and commands arrive with `forward` in slice 3. So each
37
- * composed cap is registered with an EMPTY native provider, and `getStatus`
71
+ * A reading group serves no command. An actuator helper's `switch` (and, on
72
+ * a light, `brightness` / `color` when every member binds them) fans the
73
+ * call out through `actuatorDispatcher` (D944). `getStatus`
38
74
  * (R4) is the kernel's: `registerNativeCap` installs a default that reads the
39
75
  * device's runtime slice for any cap declaring both `status` and
40
76
  * `runtimeState` (`device-cap-proxy.ts`, "Default `getStatus`"), on the
@@ -50,6 +86,13 @@ function parseDeviceStateChanged(data) {
50
86
  * declaration is the authority for what this device is.
51
87
  */
52
88
  var COMPOSED_DEVICE_CONFIG_SCHEMA = zod.z.object({});
89
+ function priorResults(slice) {
90
+ if (!isRecord$2(slice) || !Array.isArray(slice.results)) return [];
91
+ return slice.results.flatMap((row) => {
92
+ const parsed = require_dist.GroupCommandResultSchema.safeParse(row);
93
+ return parsed.success ? [parsed.data] : [];
94
+ });
95
+ }
53
96
  function isRecord$2(value) {
54
97
  return typeof value === "object" && value !== null && !Array.isArray(value);
55
98
  }
@@ -62,7 +105,11 @@ function sliceBoolean(slice, field) {
62
105
  throw new Error(`composed ${field} is unknown — not a stand-in value`);
63
106
  }
64
107
  var ComposedDevice = class extends require_dist.BaseDevice {
65
- features = [];
108
+ sharedFeatures = [];
109
+ extraCaps = /* @__PURE__ */ new Set();
110
+ get features() {
111
+ return this.sharedFeatures;
112
+ }
66
113
  constructor(ctx, type, caps) {
67
114
  super(ctx, COMPOSED_DEVICE_CONFIG_SCHEMA, { type });
68
115
  for (const cap of caps) {
@@ -71,10 +118,80 @@ var ComposedDevice = class extends require_dist.BaseDevice {
71
118
  ctx.registerNativeCap(require_dist.motionCapability, { isDetected: async () => sliceBoolean(read(), "detected") });
72
119
  continue;
73
120
  }
121
+ if (cap.name === require_dist.switchCapability.name) {
122
+ ctx.registerNativeCap(require_dist.switchCapability, { setState: async (input) => {
123
+ await this.dispatch("switch", "setState", { on: input.on });
124
+ } });
125
+ continue;
126
+ }
74
127
  ctx.registerNativeCap(cap, {});
75
128
  }
129
+ if (type === require_dist.DeviceType.Switch || type === require_dist.DeviceType.Light) ctx.registerNativeCap(require_dist.groupCommandsCapability, {});
76
130
  this.markOnline(false);
77
131
  }
132
+ /**
133
+ * Register or drop brightness and colour, and publish the shared colour
134
+ * modes. Switch stays: it is the active state, registered with the device.
135
+ */
136
+ syncShared(caps, modes) {
137
+ this.ensureExtra(require_dist.brightnessCapability.name, caps.includes(require_dist.brightnessCapability.name), () => {
138
+ this.ctx.registerNativeCap(require_dist.brightnessCapability, { setBrightness: async (input) => {
139
+ await this.dispatch("brightness", "setBrightness", { percentage: input.percentage });
140
+ } });
141
+ });
142
+ this.ensureExtra(require_dist.colorCapability.name, caps.includes(require_dist.colorCapability.name), () => {
143
+ this.ctx.registerNativeCap(require_dist.colorCapability, { setColor: async (input) => {
144
+ await this.dispatch("color", "setColor", { color: input.color });
145
+ } });
146
+ });
147
+ this.sharedFeatures = [...modes];
148
+ this.refreshFeatures();
149
+ }
150
+ /** Keep each member's latest attempt. A later command replaces that member only. */
151
+ recordCommands(incoming) {
152
+ const prior = priorResults(this.runtimeState.getCapState(require_dist.groupCommandsCapability.name));
153
+ const byDevice = new Map(prior.map((row) => [row.deviceId, row]));
154
+ for (const row of incoming) byDevice.set(row.deviceId, row);
155
+ try {
156
+ this.runtimeState.setCapState(require_dist.groupCommandsCapability.name, { results: [...byDevice.values()] });
157
+ } catch (err) {
158
+ this.ctx.logger.error("actuator group could not store command results", {
159
+ tags: { deviceId: this.id },
160
+ meta: { error: require_dist.errMsg(err) }
161
+ });
162
+ }
163
+ }
164
+ ensureExtra(capName, want, register) {
165
+ const has = this.extraCaps.has(capName);
166
+ if (want && !has) {
167
+ register();
168
+ this.extraCaps = new Set([...this.extraCaps, capName]);
169
+ return;
170
+ }
171
+ if (!want && has) {
172
+ this.ctx.unregisterNativeCap?.(capName);
173
+ this.extraCaps = new Set([...this.extraCaps].filter((name) => name !== capName));
174
+ }
175
+ }
176
+ async dispatch(cap, method, args) {
177
+ const run = actuatorDispatcher();
178
+ if (run === null) {
179
+ this.ctx.logger.warn("actuator group command was not sent — fan-out is not attached", {
180
+ tags: { deviceId: this.id },
181
+ meta: {
182
+ cap,
183
+ method
184
+ }
185
+ });
186
+ throw new Error("actuator fan-out is not attached");
187
+ }
188
+ await run({
189
+ groupDeviceId: this.id,
190
+ cap,
191
+ method,
192
+ args
193
+ });
194
+ }
78
195
  seed(capName, slice) {
79
196
  try {
80
197
  this.runtimeState.setCapState(capName, { ...slice });
@@ -244,6 +361,223 @@ var ClaimGate = class {
244
361
  }
245
362
  };
246
363
  //#endregion
364
+ //#region src/builtins/composer/actuator-groups.ts
365
+ /**
366
+ * Live actuator helpers (D944). State is the aggregate the composer already
367
+ * runs. Commands are fanned out here: every member that binds the cap,
368
+ * including one that is offline, and a failure on one does not cancel the rest.
369
+ * A light's brightness and colour are registered only while every member binds
370
+ * them, and recomputed when a member's bindings change.
371
+ */
372
+ function isActuatorGroupDevice(device) {
373
+ return "syncShared" in device && "recordCommands" in device && "id" in device;
374
+ }
375
+ var ActuatorGroups = class {
376
+ deps;
377
+ groups = /* @__PURE__ */ new Map();
378
+ bound;
379
+ constructor(deps) {
380
+ this.deps = deps;
381
+ this.bound = (input) => this.command(input);
382
+ setActuatorDispatcher(this.bound);
383
+ }
384
+ shutdown() {
385
+ setActuatorDispatcher(null);
386
+ }
387
+ async sync(blocks) {
388
+ const next = /* @__PURE__ */ new Map();
389
+ for (const block of blocks) {
390
+ const remembered = await this.remember(block);
391
+ if (remembered !== null) next.set(block.id, remembered);
392
+ }
393
+ this.groups = next;
394
+ }
395
+ async onMemberBindings(deviceId) {
396
+ const hit = [...this.groups.values()].filter((group) => group.fanOut.some((member) => member.deviceId === deviceId));
397
+ if (hit.length === 0) return;
398
+ const next = new Map(this.groups);
399
+ for (const group of hit) {
400
+ const block = {
401
+ id: group.blockId,
402
+ helper: {
403
+ kind: "group",
404
+ templateId: group.templateId,
405
+ aggregate: "any",
406
+ active: "at-least-one",
407
+ members: group.members.map(copyMember)
408
+ }
409
+ };
410
+ const remembered = await this.remember(block);
411
+ if (remembered !== null) next.set(group.blockId, remembered);
412
+ }
413
+ this.groups = next;
414
+ }
415
+ async remember(block) {
416
+ const spec = block.helper;
417
+ if (spec === void 0) return null;
418
+ const template = require_dist.groupTemplate(spec.templateId);
419
+ if (template === null || template.family !== "actuator") return null;
420
+ if (template.id !== "switch" && template.id !== "light") return null;
421
+ const device = this.deps.deviceOf(block.id);
422
+ if (device === null) return null;
423
+ const viewed = await this.viewMembers(spec.members, device.id);
424
+ const shared = sharedOf(template.id, viewed);
425
+ const previous = this.groups.get(block.id)?.shared;
426
+ if (template.id === "light" && previous !== void 0) this.logCapChange(device.id, previous, shared, viewed);
427
+ const modes = template.id === "light" && shared.includes("color") ? require_dist.sharedColorModes(viewed.featureLists) : [];
428
+ device.syncShared(shared, modes);
429
+ return {
430
+ blockId: block.id,
431
+ templateId: template.id,
432
+ groupDeviceId: device.id,
433
+ members: spec.members.map(copyMember),
434
+ fanOut: viewed.fanOut,
435
+ shared
436
+ };
437
+ }
438
+ async viewMembers(members, groupDeviceId) {
439
+ const fanOut = [];
440
+ const capLists = [];
441
+ const featureLists = [];
442
+ let complete = true;
443
+ for (const member of members) {
444
+ const resolved = this.deps.resolve(member.source);
445
+ if (resolved.kind !== "found") {
446
+ complete = false;
447
+ this.deps.logger.warn("actuator group left a member out of shared commands — its device was not resolved", {
448
+ tags: { deviceId: groupDeviceId },
449
+ meta: { source: `${member.source.addonId}/${member.source.stableId}` }
450
+ });
451
+ continue;
452
+ }
453
+ const read = await this.deps.readMember(resolved.deviceId);
454
+ if (read.kind !== "ready") {
455
+ complete = false;
456
+ fanOut.push({
457
+ deviceId: resolved.deviceId,
458
+ caps: [],
459
+ online: true
460
+ });
461
+ this.deps.logger.warn("actuator group left a member out of shared commands — its capabilities could not be read", {
462
+ tags: { deviceId: resolved.deviceId },
463
+ meta: {
464
+ groupDeviceId,
465
+ error: read.error
466
+ }
467
+ });
468
+ continue;
469
+ }
470
+ capLists.push(read.caps);
471
+ featureLists.push(read.features);
472
+ fanOut.push({
473
+ deviceId: resolved.deviceId,
474
+ caps: read.caps,
475
+ online: true
476
+ });
477
+ }
478
+ return {
479
+ fanOut,
480
+ capLists: complete ? capLists : null,
481
+ featureLists
482
+ };
483
+ }
484
+ logCapChange(groupDeviceId, previous, shared, viewed) {
485
+ for (const cap of ["brightness", "color"]) {
486
+ const had = previous.includes(cap);
487
+ const has = shared.includes(cap);
488
+ if (had === has) continue;
489
+ if (has) {
490
+ this.deps.logger.info("light group gained a shared capability", {
491
+ tags: { deviceId: groupDeviceId },
492
+ meta: { cap }
493
+ });
494
+ continue;
495
+ }
496
+ this.deps.logger.warn("light group dropped a capability a member does not bind", {
497
+ tags: { deviceId: groupDeviceId },
498
+ meta: { cap }
499
+ });
500
+ for (const member of viewed.fanOut) {
501
+ if (member.caps.includes(cap)) continue;
502
+ this.deps.logger.warn("light group dropped a capability this member does not bind", {
503
+ tags: { deviceId: member.deviceId },
504
+ meta: {
505
+ groupDeviceId,
506
+ cap
507
+ }
508
+ });
509
+ }
510
+ }
511
+ }
512
+ async command(input) {
513
+ const group = [...this.groups.values()].find((item) => item.groupDeviceId === input.groupDeviceId);
514
+ if (group === void 0) {
515
+ this.deps.logger.warn("actuator command dropped — this device is not a live helper group", {
516
+ tags: { deviceId: input.groupDeviceId },
517
+ meta: {
518
+ cap: input.cap,
519
+ method: input.method
520
+ }
521
+ });
522
+ throw new Error("actuator group is not live");
523
+ }
524
+ if (!group.shared.includes(input.cap)) {
525
+ this.deps.logger.warn("actuator command dropped — the group does not share that capability", {
526
+ tags: { deviceId: input.groupDeviceId },
527
+ meta: {
528
+ cap: input.cap,
529
+ method: input.method
530
+ }
531
+ });
532
+ throw new Error(`group does not share ${input.cap}`);
533
+ }
534
+ const results = await require_dist.runFanOut(require_dist.fanOutTargets(group.fanOut, input.cap, input.method, input.args), (call) => this.deps.invoke(call));
535
+ const failed = results.flatMap((result) => result.ok ? [] : [result]);
536
+ for (const result of failed) this.deps.logger.warn("actuator group command failed on a member — the others were still attempted", {
537
+ tags: { deviceId: result.deviceId },
538
+ meta: {
539
+ groupDeviceId: input.groupDeviceId,
540
+ cap: input.cap,
541
+ method: input.method,
542
+ error: result.error
543
+ }
544
+ });
545
+ if (failed.length > 0) this.deps.logger.warn("actuator group command finished with member failures", {
546
+ tags: { deviceId: input.groupDeviceId },
547
+ meta: {
548
+ failed: failed.length,
549
+ attempted: results.length,
550
+ cap: input.cap
551
+ }
552
+ });
553
+ const at = this.deps.now();
554
+ this.deps.deviceOf(group.blockId)?.recordCommands(results.map((result) => ({
555
+ deviceId: result.deviceId,
556
+ cap: input.cap,
557
+ method: input.method,
558
+ ok: result.ok,
559
+ ...result.ok ? {} : { error: result.error },
560
+ at
561
+ })));
562
+ }
563
+ };
564
+ function sharedOf(templateId, viewed) {
565
+ if (templateId === "switch") return ["switch"];
566
+ if (viewed.capLists === null) return ["switch"];
567
+ const shared = require_dist.sharedLightCaps(viewed.capLists);
568
+ return shared.includes("switch") ? shared : ["switch", ...shared];
569
+ }
570
+ function copyMember(member) {
571
+ return {
572
+ source: {
573
+ addonId: member.source.addonId,
574
+ stableId: member.source.stableId
575
+ },
576
+ cap: member.cap,
577
+ fieldPath: member.fieldPath
578
+ };
579
+ }
580
+ //#endregion
247
581
  //#region src/builtins/composer/composed-device-set.ts
248
582
  /**
249
583
  * The composed devices the composer DECLARED (D659): one row per `new`-target
@@ -3275,6 +3609,7 @@ var Composer = class {
3275
3609
  claimGate;
3276
3610
  targets;
3277
3611
  grafts;
3612
+ actuators;
3278
3613
  /** The rows the composer owns, read ONCE before its first withdrawal decision. */
3279
3614
  rowSeed;
3280
3615
  /**
@@ -3306,6 +3641,7 @@ var Composer = class {
3306
3641
  this.grafts.describing(deviceId);
3307
3642
  return deps.describeTarget(deviceId);
3308
3643
  });
3644
+ this.actuators = this.attachActuators(deps);
3309
3645
  this.waits = new ApplyWaits(deps.logger);
3310
3646
  this.rowSeed = composedRowSeed({
3311
3647
  logger: deps.logger,
@@ -3357,9 +3693,33 @@ var Composer = class {
3357
3693
  this.targets.invalidate(data.deviceId);
3358
3694
  this.grafts.onBindingsChanged(data);
3359
3695
  this.dispatch(await this.deps.tracker.onBindingsChanged(data.deviceId, data.capName, data.reason));
3696
+ await this.chained(() => this.actuators?.onMemberBindings(data.deviceId));
3360
3697
  }
3361
3698
  shutdown() {
3362
3699
  for (const entry of this.live.values()) entry.runtime?.stop();
3700
+ this.actuators?.shutdown();
3701
+ }
3702
+ attachActuators(deps) {
3703
+ const read = deps.readActuatorMember;
3704
+ const invoke = deps.invokeActuator;
3705
+ if (read === void 0 || invoke === void 0) return null;
3706
+ return new ActuatorGroups({
3707
+ logger: deps.logger.child("actuators"),
3708
+ now: deps.now,
3709
+ resolve: (ref) => {
3710
+ const found = deps.tracker.resolution(ref);
3711
+ return found.kind === "found" ? {
3712
+ kind: "found",
3713
+ deviceId: found.deviceId
3714
+ } : { kind: "other" };
3715
+ },
3716
+ readMember: read,
3717
+ deviceOf: (blockId) => {
3718
+ const device = this.composed.get(require_dist.composedDeviceStableId(blockId));
3719
+ return device !== null && isActuatorGroupDevice(device) ? device : null;
3720
+ },
3721
+ invoke
3722
+ });
3363
3723
  }
3364
3724
  /** Run `work` after every apply already queued, whether that settled or failed. */
3365
3725
  chained(work) {
@@ -3393,6 +3753,10 @@ var Composer = class {
3393
3753
  await Promise.all(rebuilt.stopped.map((runtime) => runtime.quiesce()));
3394
3754
  const unreleasedClaims = await this.releaseConfirmed(releasing);
3395
3755
  await this.syncSources(rebuilt);
3756
+ await this.actuators?.sync(input.blocks.map((block) => ({
3757
+ id: block.id,
3758
+ helper: block.composition.helper
3759
+ })));
3396
3760
  await this.reconcileClaims();
3397
3761
  return {
3398
3762
  blocks: planned.map((p) => this.verdict(p.block.id)),
@@ -4824,7 +5188,43 @@ var ComposerAddon = class extends require_dist.BaseAddon {
4824
5188
  describeTarget: readBoundCaps,
4825
5189
  grafts,
4826
5190
  now: () => Date.now(),
4827
- newTimer: () => new NodeDeadlineTimer(() => Date.now())
5191
+ newTimer: () => new NodeDeadlineTimer(() => Date.now()),
5192
+ readActuatorMember: async (deviceId) => {
5193
+ try {
5194
+ const bindings = await api.deviceManager.getBindings.query({ deviceId });
5195
+ const found = await api.deviceManager.getDevice.query({ deviceId });
5196
+ return {
5197
+ kind: "ready",
5198
+ caps: bindings.entries.map((entry) => entry.capName),
5199
+ features: found === null ? [] : [...found.features]
5200
+ };
5201
+ } catch (err) {
5202
+ return {
5203
+ kind: "unreadable",
5204
+ error: require_dist.errMsg(err)
5205
+ };
5206
+ }
5207
+ },
5208
+ invokeActuator: (call) => invokeActuatorCall({
5209
+ switchSetState: async (deviceId, on) => {
5210
+ await api.switch.setState.mutate({
5211
+ deviceId,
5212
+ on
5213
+ });
5214
+ },
5215
+ setBrightness: async (deviceId, percentage) => {
5216
+ await api.brightness.setBrightness.mutate({
5217
+ deviceId,
5218
+ percentage
5219
+ });
5220
+ },
5221
+ setColor: async (deviceId, color) => {
5222
+ await api.color.setColor.mutate({
5223
+ deviceId,
5224
+ color
5225
+ });
5226
+ }
5227
+ }, call)
4828
5228
  });
4829
5229
  this.subscribe({ category: require_dist.EventCategory.DeviceStateChanged }, (event) => {
4830
5230
  const data = parseDeviceStateChanged(event.data);