@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,6 +1,6 @@
|
|
|
1
1
|
let zod = require("zod");
|
|
2
2
|
require("node:crypto");
|
|
3
|
-
//#region ../types/dist/event-category-
|
|
3
|
+
//#region ../types/dist/event-category-CT-hz5Rb.mjs
|
|
4
4
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
5
5
|
EventCategory["SystemBoot"] = "system.boot";
|
|
6
6
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -326,6 +326,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
326
326
|
/** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
|
|
327
327
|
EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
|
|
328
328
|
/**
|
|
329
|
+
* A camera's package LLM confirm has failed closed three times in a row
|
|
330
|
+
* (D931, 2026-10-26). One edge per camera until
|
|
331
|
+
* {@link PipelinePackageLlmAnswered}.
|
|
332
|
+
*/
|
|
333
|
+
EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
|
|
334
|
+
/** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
|
|
335
|
+
EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
|
|
336
|
+
/**
|
|
329
337
|
* Per-camera pipeline config was mutated by the orchestrator
|
|
330
338
|
* (a settings change via `setAgentInferenceDevices` /
|
|
331
339
|
* `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
|
|
@@ -765,7 +773,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
765
773
|
return EventCategory;
|
|
766
774
|
}({});
|
|
767
775
|
//#endregion
|
|
768
|
-
//#region ../types/dist/sleep-
|
|
776
|
+
//#region ../types/dist/sleep-bykKrTgu.mjs
|
|
769
777
|
/**
|
|
770
778
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
771
779
|
* window to float samples (D455).
|
|
@@ -5183,6 +5191,8 @@ var CAP_NAMES = Object.freeze([
|
|
|
5183
5191
|
"filesystem-browse",
|
|
5184
5192
|
"flood",
|
|
5185
5193
|
"gas",
|
|
5194
|
+
"group-commands",
|
|
5195
|
+
"group-status",
|
|
5186
5196
|
"heating-valve",
|
|
5187
5197
|
"humidifier",
|
|
5188
5198
|
"humidity-sensor",
|
|
@@ -6001,6 +6011,20 @@ Object.freeze({
|
|
|
6001
6011
|
"path": "detected",
|
|
6002
6012
|
"valueType": "boolean"
|
|
6003
6013
|
}],
|
|
6014
|
+
"group-status": [
|
|
6015
|
+
{
|
|
6016
|
+
"path": "included",
|
|
6017
|
+
"valueType": "number"
|
|
6018
|
+
},
|
|
6019
|
+
{
|
|
6020
|
+
"path": "excluded",
|
|
6021
|
+
"valueType": "number"
|
|
6022
|
+
},
|
|
6023
|
+
{
|
|
6024
|
+
"path": "total",
|
|
6025
|
+
"valueType": "number"
|
|
6026
|
+
}
|
|
6027
|
+
],
|
|
6004
6028
|
"heating-valve": [{
|
|
6005
6029
|
"path": "position",
|
|
6006
6030
|
"valueType": "number"
|
|
@@ -7468,7 +7492,7 @@ function errMsg(err) {
|
|
|
7468
7492
|
return String(err);
|
|
7469
7493
|
}
|
|
7470
7494
|
//#endregion
|
|
7471
|
-
//#region ../types/dist/composition-
|
|
7495
|
+
//#region ../types/dist/composition-OhpuespA.mjs
|
|
7472
7496
|
var AUDIO_ENCODER_BY_CODEC = {
|
|
7473
7497
|
opus: "libopus",
|
|
7474
7498
|
aac: "aac",
|
|
@@ -8986,6 +9010,75 @@ function evaluateExpressionSource(expr, bindingValues, now, opts) {
|
|
|
8986
9010
|
}
|
|
8987
9011
|
}
|
|
8988
9012
|
/**
|
|
9013
|
+
* A composition source identity: `(addonId, stableId)`, never a numeric device
|
|
9014
|
+
* id. Split out so helper schemas can name it without a cycle through
|
|
9015
|
+
* `composition.ts` (D659).
|
|
9016
|
+
*/
|
|
9017
|
+
var CompositionSourceRefSchema = zod.z.object({
|
|
9018
|
+
/** The owning addon; `stableId` is unique only within it. */
|
|
9019
|
+
addonId: zod.z.string().min(1),
|
|
9020
|
+
stableId: zod.z.string().min(1)
|
|
9021
|
+
});
|
|
9022
|
+
var GROUP_TEMPLATE_IDS = [
|
|
9023
|
+
"temperature",
|
|
9024
|
+
"humidity",
|
|
9025
|
+
"pressure",
|
|
9026
|
+
"illuminance",
|
|
9027
|
+
"power",
|
|
9028
|
+
"energy",
|
|
9029
|
+
"battery",
|
|
9030
|
+
"numeric",
|
|
9031
|
+
"motion",
|
|
9032
|
+
"contact",
|
|
9033
|
+
"leak",
|
|
9034
|
+
"smoke",
|
|
9035
|
+
"occupancy",
|
|
9036
|
+
"binary",
|
|
9037
|
+
"switch",
|
|
9038
|
+
"light"
|
|
9039
|
+
];
|
|
9040
|
+
var NUMERIC_AGGREGATES = [
|
|
9041
|
+
"mean",
|
|
9042
|
+
"median",
|
|
9043
|
+
"min",
|
|
9044
|
+
"max",
|
|
9045
|
+
"sum"
|
|
9046
|
+
];
|
|
9047
|
+
var BINARY_AGGREGATES = ["any", "all"];
|
|
9048
|
+
/** How an actuator group decides it is on. Unknown members never count as off. */
|
|
9049
|
+
var ACTIVE_CRITERIA = ["at-least-one", "all"];
|
|
9050
|
+
var AGGREGATE_REDUCERS = [
|
|
9051
|
+
"mean",
|
|
9052
|
+
"median",
|
|
9053
|
+
"min",
|
|
9054
|
+
"max",
|
|
9055
|
+
"sum",
|
|
9056
|
+
"any",
|
|
9057
|
+
"all"
|
|
9058
|
+
];
|
|
9059
|
+
var GroupTemplateIdSchema = zod.z.enum(GROUP_TEMPLATE_IDS);
|
|
9060
|
+
var AggregateReducerSchema = zod.z.enum(AGGREGATE_REDUCERS);
|
|
9061
|
+
var ActiveCriterionSchema = zod.z.enum(ACTIVE_CRITERIA);
|
|
9062
|
+
/** One member. The field is stored so occupancy can name motion or binary. */
|
|
9063
|
+
var HelperMemberSchema = zod.z.object({
|
|
9064
|
+
source: CompositionSourceRefSchema,
|
|
9065
|
+
cap: zod.z.string().min(1),
|
|
9066
|
+
fieldPath: zod.z.string().min(1)
|
|
9067
|
+
});
|
|
9068
|
+
/**
|
|
9069
|
+
* Phase 1a is groups only. Math, integral and statistics stay off this union
|
|
9070
|
+
* until the composer can run them — a control must not save a spec it cannot.
|
|
9071
|
+
*/
|
|
9072
|
+
var GroupHelperSpecSchema = zod.z.object({
|
|
9073
|
+
kind: zod.z.literal("group"),
|
|
9074
|
+
templateId: GroupTemplateIdSchema,
|
|
9075
|
+
aggregate: AggregateReducerSchema,
|
|
9076
|
+
members: zod.z.array(HelperMemberSchema).min(1).max(64),
|
|
9077
|
+
/** Actuator groups only. Readings leave this unset. */
|
|
9078
|
+
active: ActiveCriterionSchema.optional()
|
|
9079
|
+
});
|
|
9080
|
+
var HelperSpecSchema = zod.z.discriminatedUnion("kind", [GroupHelperSpecSchema]);
|
|
9081
|
+
/**
|
|
8989
9082
|
* A COMPOSITION: a device assembled from other devices' state (D659).
|
|
8990
9083
|
*
|
|
8991
9084
|
* The data half of the composed-devices spec §1. A composition is stored inside
|
|
@@ -9010,11 +9103,6 @@ var COMPOSER_ADDON_ID = "composer";
|
|
|
9010
9103
|
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
9011
9104
|
/** Longest snippet a `code` source may carry (slice 4). */
|
|
9012
9105
|
var MAX_COMPOSITION_CODE_LENGTH = 2e4;
|
|
9013
|
-
var CompositionSourceRefSchema = zod.z.object({
|
|
9014
|
-
/** The owning addon; `stableId` is unique only within it. */
|
|
9015
|
-
addonId: zod.z.string().min(1),
|
|
9016
|
-
stableId: zod.z.string().min(1)
|
|
9017
|
-
});
|
|
9018
9106
|
/** One field of one capability of one source device. */
|
|
9019
9107
|
var CompositionFieldReadSchema = zod.z.object({
|
|
9020
9108
|
source: CompositionSourceRefSchema,
|
|
@@ -9057,10 +9145,42 @@ var CompositionCodeSourceSchema = zod.z.object({
|
|
|
9057
9145
|
kind: zod.z.literal("code"),
|
|
9058
9146
|
code: zod.z.string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
|
|
9059
9147
|
});
|
|
9148
|
+
/**
|
|
9149
|
+
* `aggregate`: a group reducer. Offline and unknown reads are excluded, not
|
|
9150
|
+
* folded in as 0 or false (D944). Free expressions stay `expression` so a
|
|
9151
|
+
* stale member is still included there.
|
|
9152
|
+
*/
|
|
9153
|
+
var CompositionAggregateSourceSchema = zod.z.object({
|
|
9154
|
+
kind: zod.z.literal("aggregate"),
|
|
9155
|
+
reducer: zod.z.enum([
|
|
9156
|
+
"mean",
|
|
9157
|
+
"median",
|
|
9158
|
+
"min",
|
|
9159
|
+
"max",
|
|
9160
|
+
"sum",
|
|
9161
|
+
"any",
|
|
9162
|
+
"all"
|
|
9163
|
+
]),
|
|
9164
|
+
reads: zod.z.array(CompositionFieldReadSchema).min(1).max(64),
|
|
9165
|
+
/** `value` is the reading; the others are the member counts. */
|
|
9166
|
+
publish: zod.z.enum([
|
|
9167
|
+
"value",
|
|
9168
|
+
"included",
|
|
9169
|
+
"excluded",
|
|
9170
|
+
"total"
|
|
9171
|
+
]),
|
|
9172
|
+
/** What a `value` publish does when nobody is included. Counts ignore this. */
|
|
9173
|
+
empty: zod.z.enum([
|
|
9174
|
+
"null",
|
|
9175
|
+
"omit",
|
|
9176
|
+
"offline"
|
|
9177
|
+
])
|
|
9178
|
+
});
|
|
9060
9179
|
var CompositionFieldSourceSchema = zod.z.discriminatedUnion("kind", [
|
|
9061
9180
|
CompositionFromSourceSchema,
|
|
9062
9181
|
CompositionExpressionSourceSchema,
|
|
9063
|
-
CompositionCodeSourceSchema
|
|
9182
|
+
CompositionCodeSourceSchema,
|
|
9183
|
+
CompositionAggregateSourceSchema
|
|
9064
9184
|
]);
|
|
9065
9185
|
/** `forward`: RESERVED for slice 3. Argument mapping arrives there as an additive optional `args`. */
|
|
9066
9186
|
var CompositionForwardCommandSchema = zod.z.object({
|
|
@@ -9125,7 +9245,13 @@ var CompositionExistingTargetSchema = zod.z.object({
|
|
|
9125
9245
|
var CompositionTargetSchema = zod.z.discriminatedUnion("kind", [CompositionNewTargetSchema, CompositionExistingTargetSchema]);
|
|
9126
9246
|
var CompositionSchema = zod.z.object({
|
|
9127
9247
|
target: CompositionTargetSchema,
|
|
9128
|
-
features: zod.z.array(CompositionFeatureSchema).min(1).max(16)
|
|
9248
|
+
features: zod.z.array(CompositionFeatureSchema).min(1).max(16),
|
|
9249
|
+
/**
|
|
9250
|
+
* Present when the block is a helper template. Absent on a free
|
|
9251
|
+
* composition (the room example, device 4603). The editor reads this,
|
|
9252
|
+
* never the compiled features.
|
|
9253
|
+
*/
|
|
9254
|
+
helper: HelperSpecSchema.optional()
|
|
9129
9255
|
}).superRefine((composition, ctx) => {
|
|
9130
9256
|
const seen = /* @__PURE__ */ new Set();
|
|
9131
9257
|
composition.features.forEach((feature, index) => {
|
|
@@ -9172,6 +9298,7 @@ function fieldReads(source) {
|
|
|
9172
9298
|
cap: binding.cap,
|
|
9173
9299
|
fieldPath: binding.fieldPath
|
|
9174
9300
|
}] : []);
|
|
9301
|
+
case "aggregate": return source.reads;
|
|
9175
9302
|
case "code": return [];
|
|
9176
9303
|
}
|
|
9177
9304
|
}
|
|
@@ -17363,7 +17490,10 @@ var CompositionProblemCodeSchema = zod.z.enum([
|
|
|
17363
17490
|
"target-missing",
|
|
17364
17491
|
"target-unreadable",
|
|
17365
17492
|
"not-replaceable",
|
|
17366
|
-
"native-capability-replace"
|
|
17493
|
+
"native-capability-replace",
|
|
17494
|
+
"mixed-quantity",
|
|
17495
|
+
"helper-mismatch",
|
|
17496
|
+
"member-constraint"
|
|
17367
17497
|
]);
|
|
17368
17498
|
/** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
|
|
17369
17499
|
var CompositionProblemSchema = zod.z.object({
|
|
@@ -17387,7 +17517,8 @@ var CompositionUnavailableModeSchema = zod.z.enum([
|
|
|
17387
17517
|
"null",
|
|
17388
17518
|
"offline",
|
|
17389
17519
|
"unknown",
|
|
17390
|
-
"release"
|
|
17520
|
+
"release",
|
|
17521
|
+
"omit"
|
|
17391
17522
|
]);
|
|
17392
17523
|
/**
|
|
17393
17524
|
* `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
|
|
@@ -17749,6 +17880,24 @@ function inferExpressionKinds(input) {
|
|
|
17749
17880
|
* planner (`composition-items.ts`). Internal to the composition module — NOT
|
|
17750
17881
|
* in the barrel: these are planner internals, not a published contract.
|
|
17751
17882
|
*/
|
|
17883
|
+
function aggregateKinds(source) {
|
|
17884
|
+
if (source.publish !== "value") return {
|
|
17885
|
+
ok: true,
|
|
17886
|
+
kinds: new Set(["number"])
|
|
17887
|
+
};
|
|
17888
|
+
if (source.reducer === "any" || source.reducer === "all") return {
|
|
17889
|
+
ok: true,
|
|
17890
|
+
kinds: new Set(["boolean"])
|
|
17891
|
+
};
|
|
17892
|
+
if (source.empty === "null") return {
|
|
17893
|
+
ok: true,
|
|
17894
|
+
kinds: new Set(["number", "null"])
|
|
17895
|
+
};
|
|
17896
|
+
return {
|
|
17897
|
+
ok: true,
|
|
17898
|
+
kinds: new Set(["number"])
|
|
17899
|
+
};
|
|
17900
|
+
}
|
|
17752
17901
|
function readKinds(read, lookupCap) {
|
|
17753
17902
|
const def = lookupCap(read.cap);
|
|
17754
17903
|
const schema = def?.runtimeState ?? def?.status?.schema;
|
|
@@ -17776,6 +17925,7 @@ function sourceKinds(source, lookupCap) {
|
|
|
17776
17925
|
code: "reserved-variant",
|
|
17777
17926
|
message: "code fields arrive in slice 4"
|
|
17778
17927
|
};
|
|
17928
|
+
if (source.kind === "aggregate") return aggregateKinds(source);
|
|
17779
17929
|
const bindingKinds = /* @__PURE__ */ new Map();
|
|
17780
17930
|
for (const [name, binding] of Object.entries(source.bindings)) {
|
|
17781
17931
|
if (binding.kind === "literal") {
|
|
@@ -17970,6 +18120,515 @@ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
|
|
|
17970
18120
|
};
|
|
17971
18121
|
}
|
|
17972
18122
|
/**
|
|
18123
|
+
* One catalog row per base reading a group helper can build (D944 §3).
|
|
18124
|
+
* Data, not a form default written into the block.
|
|
18125
|
+
*/
|
|
18126
|
+
var NOW = {
|
|
18127
|
+
fieldPath: "lastFetchedAt",
|
|
18128
|
+
expr: "now"
|
|
18129
|
+
};
|
|
18130
|
+
var CHANGED = {
|
|
18131
|
+
fieldPath: "lastChangedAt",
|
|
18132
|
+
expr: "now"
|
|
18133
|
+
};
|
|
18134
|
+
function numeric(input) {
|
|
18135
|
+
return {
|
|
18136
|
+
...input,
|
|
18137
|
+
family: "numeric",
|
|
18138
|
+
deviceType: DeviceType.Sensor,
|
|
18139
|
+
memberDeviceType: DeviceType.Sensor,
|
|
18140
|
+
commandCaps: [],
|
|
18141
|
+
baseUnit: baseUnitOf(input.quantity),
|
|
18142
|
+
aggregates: NUMERIC_AGGREGATES,
|
|
18143
|
+
accept: [{
|
|
18144
|
+
cap: input.outputCap,
|
|
18145
|
+
fieldPath: input.outputField
|
|
18146
|
+
}]
|
|
18147
|
+
};
|
|
18148
|
+
}
|
|
18149
|
+
function binary(input) {
|
|
18150
|
+
return {
|
|
18151
|
+
...input,
|
|
18152
|
+
family: "binary",
|
|
18153
|
+
deviceType: DeviceType.Sensor,
|
|
18154
|
+
memberDeviceType: DeviceType.Sensor,
|
|
18155
|
+
commandCaps: [],
|
|
18156
|
+
quantity: "none",
|
|
18157
|
+
baseUnit: void 0,
|
|
18158
|
+
aggregates: BINARY_AGGREGATES,
|
|
18159
|
+
empty: "offline"
|
|
18160
|
+
};
|
|
18161
|
+
}
|
|
18162
|
+
var GROUP_TEMPLATES = [
|
|
18163
|
+
numeric({
|
|
18164
|
+
id: "temperature",
|
|
18165
|
+
summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
|
|
18166
|
+
role: DeviceRole.TemperatureSensor,
|
|
18167
|
+
outputCap: "temperature-sensor",
|
|
18168
|
+
outputField: "celsius",
|
|
18169
|
+
quantity: "temperature",
|
|
18170
|
+
defaultAggregate: "mean",
|
|
18171
|
+
empty: "null",
|
|
18172
|
+
companions: [NOW]
|
|
18173
|
+
}),
|
|
18174
|
+
numeric({
|
|
18175
|
+
id: "humidity",
|
|
18176
|
+
summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
18177
|
+
role: DeviceRole.HumiditySensor,
|
|
18178
|
+
outputCap: "humidity-sensor",
|
|
18179
|
+
outputField: "percent",
|
|
18180
|
+
quantity: "humidity",
|
|
18181
|
+
defaultAggregate: "mean",
|
|
18182
|
+
empty: "null",
|
|
18183
|
+
companions: [NOW]
|
|
18184
|
+
}),
|
|
18185
|
+
numeric({
|
|
18186
|
+
id: "pressure",
|
|
18187
|
+
summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
|
|
18188
|
+
role: DeviceRole.PressureSensor,
|
|
18189
|
+
outputCap: "pressure-sensor",
|
|
18190
|
+
outputField: "hpa",
|
|
18191
|
+
quantity: "pressure",
|
|
18192
|
+
defaultAggregate: "mean",
|
|
18193
|
+
empty: "null",
|
|
18194
|
+
companions: [NOW]
|
|
18195
|
+
}),
|
|
18196
|
+
numeric({
|
|
18197
|
+
id: "illuminance",
|
|
18198
|
+
summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
|
|
18199
|
+
role: DeviceRole.AmbientLightSensor,
|
|
18200
|
+
outputCap: "ambient-light-sensor",
|
|
18201
|
+
outputField: "lux",
|
|
18202
|
+
quantity: "illuminance",
|
|
18203
|
+
defaultAggregate: "mean",
|
|
18204
|
+
empty: "null",
|
|
18205
|
+
companions: [NOW]
|
|
18206
|
+
}),
|
|
18207
|
+
numeric({
|
|
18208
|
+
id: "power",
|
|
18209
|
+
summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
|
|
18210
|
+
role: DeviceRole.PowerSensor,
|
|
18211
|
+
outputCap: "power-meter",
|
|
18212
|
+
outputField: "watts",
|
|
18213
|
+
quantity: "power",
|
|
18214
|
+
defaultAggregate: "sum",
|
|
18215
|
+
empty: "omit",
|
|
18216
|
+
companions: [NOW]
|
|
18217
|
+
}),
|
|
18218
|
+
numeric({
|
|
18219
|
+
id: "energy",
|
|
18220
|
+
summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
|
|
18221
|
+
role: DeviceRole.EnergySensor,
|
|
18222
|
+
outputCap: "power-meter",
|
|
18223
|
+
outputField: "kwhTotal",
|
|
18224
|
+
quantity: "energy",
|
|
18225
|
+
defaultAggregate: "sum",
|
|
18226
|
+
empty: "omit",
|
|
18227
|
+
companions: [NOW]
|
|
18228
|
+
}),
|
|
18229
|
+
numeric({
|
|
18230
|
+
id: "battery",
|
|
18231
|
+
summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
18232
|
+
role: DeviceRole.BatterySensor,
|
|
18233
|
+
outputCap: "battery",
|
|
18234
|
+
outputField: "percentage",
|
|
18235
|
+
quantity: "percentage",
|
|
18236
|
+
defaultAggregate: "mean",
|
|
18237
|
+
empty: "null",
|
|
18238
|
+
companions: [
|
|
18239
|
+
{
|
|
18240
|
+
fieldPath: "charging",
|
|
18241
|
+
expr: "\"unknown\""
|
|
18242
|
+
},
|
|
18243
|
+
{
|
|
18244
|
+
fieldPath: "sleeping",
|
|
18245
|
+
expr: "false"
|
|
18246
|
+
},
|
|
18247
|
+
{
|
|
18248
|
+
fieldPath: "lastUpdated",
|
|
18249
|
+
expr: "now"
|
|
18250
|
+
}
|
|
18251
|
+
]
|
|
18252
|
+
}),
|
|
18253
|
+
numeric({
|
|
18254
|
+
id: "numeric",
|
|
18255
|
+
summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
|
|
18256
|
+
role: DeviceRole.NumericSensor,
|
|
18257
|
+
outputCap: "numeric-sensor",
|
|
18258
|
+
outputField: "value",
|
|
18259
|
+
quantity: "device",
|
|
18260
|
+
defaultAggregate: "mean",
|
|
18261
|
+
empty: "null",
|
|
18262
|
+
companions: [NOW]
|
|
18263
|
+
}),
|
|
18264
|
+
binary({
|
|
18265
|
+
id: "motion",
|
|
18266
|
+
summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
|
|
18267
|
+
role: DeviceRole.MotionSensor,
|
|
18268
|
+
outputCap: "motion",
|
|
18269
|
+
outputField: "detected",
|
|
18270
|
+
defaultAggregate: "any",
|
|
18271
|
+
accept: [{
|
|
18272
|
+
cap: "motion",
|
|
18273
|
+
fieldPath: "detected"
|
|
18274
|
+
}],
|
|
18275
|
+
companions: [{
|
|
18276
|
+
fieldPath: "lastDetectedAt",
|
|
18277
|
+
expr: "null"
|
|
18278
|
+
}, {
|
|
18279
|
+
fieldPath: "autoClearAfterMs",
|
|
18280
|
+
expr: "null"
|
|
18281
|
+
}]
|
|
18282
|
+
}),
|
|
18283
|
+
binary({
|
|
18284
|
+
id: "contact",
|
|
18285
|
+
summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
|
|
18286
|
+
role: DeviceRole.ContactSensor,
|
|
18287
|
+
outputCap: "contact",
|
|
18288
|
+
outputField: "entryOpen",
|
|
18289
|
+
defaultAggregate: "any",
|
|
18290
|
+
accept: [{
|
|
18291
|
+
cap: "contact",
|
|
18292
|
+
fieldPath: "entryOpen"
|
|
18293
|
+
}],
|
|
18294
|
+
companions: [CHANGED]
|
|
18295
|
+
}),
|
|
18296
|
+
binary({
|
|
18297
|
+
id: "leak",
|
|
18298
|
+
summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
|
|
18299
|
+
role: DeviceRole.LeakSensor,
|
|
18300
|
+
outputCap: "flood",
|
|
18301
|
+
outputField: "flooded",
|
|
18302
|
+
defaultAggregate: "any",
|
|
18303
|
+
accept: [{
|
|
18304
|
+
cap: "flood",
|
|
18305
|
+
fieldPath: "flooded"
|
|
18306
|
+
}],
|
|
18307
|
+
companions: [CHANGED]
|
|
18308
|
+
}),
|
|
18309
|
+
binary({
|
|
18310
|
+
id: "smoke",
|
|
18311
|
+
summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
|
|
18312
|
+
role: DeviceRole.SmokeSensor,
|
|
18313
|
+
outputCap: "smoke",
|
|
18314
|
+
outputField: "detected",
|
|
18315
|
+
defaultAggregate: "any",
|
|
18316
|
+
accept: [{
|
|
18317
|
+
cap: "smoke",
|
|
18318
|
+
fieldPath: "detected"
|
|
18319
|
+
}],
|
|
18320
|
+
companions: [CHANGED]
|
|
18321
|
+
}),
|
|
18322
|
+
binary({
|
|
18323
|
+
id: "occupancy",
|
|
18324
|
+
summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
|
|
18325
|
+
role: DeviceRole.BinarySensor,
|
|
18326
|
+
outputCap: "binary",
|
|
18327
|
+
outputField: "on",
|
|
18328
|
+
defaultAggregate: "any",
|
|
18329
|
+
accept: [{
|
|
18330
|
+
cap: "motion",
|
|
18331
|
+
fieldPath: "detected"
|
|
18332
|
+
}, {
|
|
18333
|
+
cap: "binary",
|
|
18334
|
+
fieldPath: "on"
|
|
18335
|
+
}],
|
|
18336
|
+
companions: [CHANGED]
|
|
18337
|
+
}),
|
|
18338
|
+
binary({
|
|
18339
|
+
id: "binary",
|
|
18340
|
+
summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
|
|
18341
|
+
role: DeviceRole.BinarySensor,
|
|
18342
|
+
outputCap: "binary",
|
|
18343
|
+
outputField: "on",
|
|
18344
|
+
defaultAggregate: "any",
|
|
18345
|
+
accept: [{
|
|
18346
|
+
cap: "binary",
|
|
18347
|
+
fieldPath: "on"
|
|
18348
|
+
}],
|
|
18349
|
+
companions: [CHANGED]
|
|
18350
|
+
}),
|
|
18351
|
+
actuator({
|
|
18352
|
+
id: "switch",
|
|
18353
|
+
summary: "On when at least one switch is on, or when all of them are. Offline and unknown members are left out of that decision, never read as off, and still receive the command.",
|
|
18354
|
+
deviceType: DeviceType.Switch,
|
|
18355
|
+
outputCap: "switch",
|
|
18356
|
+
commandCaps: ["switch"]
|
|
18357
|
+
}),
|
|
18358
|
+
actuator({
|
|
18359
|
+
id: "light",
|
|
18360
|
+
summary: "On when at least one light is on, or when all of them are. Brightness and colour are offered only when every member has them. A command is not sent to a member that lacks it.",
|
|
18361
|
+
deviceType: DeviceType.Light,
|
|
18362
|
+
outputCap: "switch",
|
|
18363
|
+
commandCaps: [
|
|
18364
|
+
"switch",
|
|
18365
|
+
"brightness",
|
|
18366
|
+
"color"
|
|
18367
|
+
]
|
|
18368
|
+
})
|
|
18369
|
+
];
|
|
18370
|
+
function actuator(input) {
|
|
18371
|
+
return {
|
|
18372
|
+
...input,
|
|
18373
|
+
family: "actuator",
|
|
18374
|
+
role: void 0,
|
|
18375
|
+
memberDeviceType: input.deviceType,
|
|
18376
|
+
outputField: "on",
|
|
18377
|
+
quantity: "none",
|
|
18378
|
+
baseUnit: void 0,
|
|
18379
|
+
aggregates: BINARY_AGGREGATES,
|
|
18380
|
+
defaultAggregate: "any",
|
|
18381
|
+
empty: "offline",
|
|
18382
|
+
accept: [{
|
|
18383
|
+
cap: "switch",
|
|
18384
|
+
fieldPath: "on"
|
|
18385
|
+
}],
|
|
18386
|
+
companions: [CHANGED]
|
|
18387
|
+
};
|
|
18388
|
+
}
|
|
18389
|
+
var BY_ID = new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
18390
|
+
function groupTemplate(id) {
|
|
18391
|
+
return BY_ID.get(id) ?? null;
|
|
18392
|
+
}
|
|
18393
|
+
/**
|
|
18394
|
+
* Actuator groups (D944 phase 1b). The active boolean is the binary reducer
|
|
18395
|
+
* with unknown members left out. A command is one call per member that binds
|
|
18396
|
+
* the cap, including members that are offline. One failure does not cancel
|
|
18397
|
+
* the others.
|
|
18398
|
+
*/
|
|
18399
|
+
/** Caps a light group may expose, in display order. Switch is the active state. */
|
|
18400
|
+
var LIGHT_SHARED_CAPS = [
|
|
18401
|
+
"switch",
|
|
18402
|
+
"brightness",
|
|
18403
|
+
"color"
|
|
18404
|
+
];
|
|
18405
|
+
var COLOR_MODE_FEATURES = [
|
|
18406
|
+
DeviceFeature.LightColorRgb,
|
|
18407
|
+
DeviceFeature.LightColorHsv,
|
|
18408
|
+
DeviceFeature.LightColorMired
|
|
18409
|
+
];
|
|
18410
|
+
/** `at-least-one` is any included member on; `all` is every included member on. */
|
|
18411
|
+
function reducerForActive(active) {
|
|
18412
|
+
return active === "all" ? "all" : "any";
|
|
18413
|
+
}
|
|
18414
|
+
/**
|
|
18415
|
+
* Caps every member binds, from {@link LIGHT_SHARED_CAPS}. An empty member
|
|
18416
|
+
* list shares nothing. A member that lacks colour drops colour from the group.
|
|
18417
|
+
*/
|
|
18418
|
+
function sharedLightCaps(memberCaps) {
|
|
18419
|
+
if (memberCaps.length === 0) return [];
|
|
18420
|
+
return LIGHT_SHARED_CAPS.filter((cap) => memberCaps.every((bound) => bound.includes(cap)));
|
|
18421
|
+
}
|
|
18422
|
+
/**
|
|
18423
|
+
* Colour modes every member advertises. Offline does not remove a mode:
|
|
18424
|
+
* the caller passes device features, not liveness. No members → no mode.
|
|
18425
|
+
*/
|
|
18426
|
+
function sharedColorModes(memberFeatures) {
|
|
18427
|
+
if (memberFeatures.length === 0) return [];
|
|
18428
|
+
return COLOR_MODE_FEATURES.filter((mode) => memberFeatures.every((features) => features.includes(mode)));
|
|
18429
|
+
}
|
|
18430
|
+
/**
|
|
18431
|
+
* One call per member that binds `cap`. Offline members stay in the list.
|
|
18432
|
+
* A member without the cap is not called — the command cannot reach it.
|
|
18433
|
+
*/
|
|
18434
|
+
function fanOutTargets(members, cap, method, args) {
|
|
18435
|
+
return members.flatMap((member) => member.caps.includes(cap) ? [{
|
|
18436
|
+
deviceId: member.deviceId,
|
|
18437
|
+
cap,
|
|
18438
|
+
method,
|
|
18439
|
+
args: { ...args }
|
|
18440
|
+
}] : []);
|
|
18441
|
+
}
|
|
18442
|
+
/**
|
|
18443
|
+
* Run every call. A throw is that member's result; the other calls still run.
|
|
18444
|
+
* At most {@link FAN_OUT_CONCURRENCY} are in flight.
|
|
18445
|
+
*/
|
|
18446
|
+
async function runFanOut(calls, invoke) {
|
|
18447
|
+
const results = [];
|
|
18448
|
+
let next = 0;
|
|
18449
|
+
const worker = async () => {
|
|
18450
|
+
for (;;) {
|
|
18451
|
+
const index = next;
|
|
18452
|
+
next += 1;
|
|
18453
|
+
const call = calls[index];
|
|
18454
|
+
if (call === void 0) return;
|
|
18455
|
+
try {
|
|
18456
|
+
await invoke(call);
|
|
18457
|
+
results[index] = {
|
|
18458
|
+
deviceId: call.deviceId,
|
|
18459
|
+
ok: true
|
|
18460
|
+
};
|
|
18461
|
+
} catch (err) {
|
|
18462
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
18463
|
+
results[index] = {
|
|
18464
|
+
deviceId: call.deviceId,
|
|
18465
|
+
ok: false,
|
|
18466
|
+
error
|
|
18467
|
+
};
|
|
18468
|
+
}
|
|
18469
|
+
}
|
|
18470
|
+
};
|
|
18471
|
+
const width = Math.min(4, calls.length);
|
|
18472
|
+
await Promise.all(Array.from({ length: width }, () => worker()));
|
|
18473
|
+
return results;
|
|
18474
|
+
}
|
|
18475
|
+
function sameSource(a, b) {
|
|
18476
|
+
return a.addonId === b.addonId && a.stableId === b.stableId;
|
|
18477
|
+
}
|
|
18478
|
+
function sameMember(a, b) {
|
|
18479
|
+
return sameSource(a.source, b.source) && a.cap === b.cap && a.fieldPath === b.fieldPath;
|
|
18480
|
+
}
|
|
18481
|
+
function accepted(template, member) {
|
|
18482
|
+
return template.accept.some((field) => field.cap === member.cap && field.fieldPath === member.fieldPath);
|
|
18483
|
+
}
|
|
18484
|
+
function memberProblems(template, spec) {
|
|
18485
|
+
return spec.members.flatMap((member, index) => {
|
|
18486
|
+
const at = `helper.members[${index}]`;
|
|
18487
|
+
const problems = [];
|
|
18488
|
+
if (!accepted(template, member)) {
|
|
18489
|
+
const allowed = template.accept.map((field) => `${field.cap}.${field.fieldPath}`).join(" or ");
|
|
18490
|
+
problems.push(problem("member-constraint", at, `a ${template.id} group reads ${allowed}; this member is ${member.cap}.${member.fieldPath}`));
|
|
18491
|
+
}
|
|
18492
|
+
if (spec.members.slice(0, index).some((other) => sameMember(other, member))) problems.push(problem("member-constraint", at, "this member is already in the group"));
|
|
18493
|
+
return problems;
|
|
18494
|
+
});
|
|
18495
|
+
}
|
|
18496
|
+
function aggregateSource(template, spec, reads, publish) {
|
|
18497
|
+
return {
|
|
18498
|
+
kind: "aggregate",
|
|
18499
|
+
reducer: spec.aggregate,
|
|
18500
|
+
reads: [...reads],
|
|
18501
|
+
publish,
|
|
18502
|
+
empty: template.empty
|
|
18503
|
+
};
|
|
18504
|
+
}
|
|
18505
|
+
function expressionSource(expr) {
|
|
18506
|
+
return {
|
|
18507
|
+
kind: "expression",
|
|
18508
|
+
expr,
|
|
18509
|
+
bindings: {}
|
|
18510
|
+
};
|
|
18511
|
+
}
|
|
18512
|
+
function draftOf$1(template, spec) {
|
|
18513
|
+
const reads = spec.members.map((member) => ({
|
|
18514
|
+
source: member.source,
|
|
18515
|
+
cap: member.cap,
|
|
18516
|
+
fieldPath: member.fieldPath
|
|
18517
|
+
}));
|
|
18518
|
+
const companions = Object.fromEntries(template.companions.map((companion) => [companion.fieldPath, expressionSource(companion.expr)]));
|
|
18519
|
+
return {
|
|
18520
|
+
target: {
|
|
18521
|
+
kind: "new",
|
|
18522
|
+
type: template.deviceType,
|
|
18523
|
+
...template.role === void 0 ? {} : { role: template.role }
|
|
18524
|
+
},
|
|
18525
|
+
helper: spec,
|
|
18526
|
+
features: [{
|
|
18527
|
+
kind: "fields",
|
|
18528
|
+
cap: template.outputCap,
|
|
18529
|
+
fields: {
|
|
18530
|
+
[template.outputField]: aggregateSource(template, spec, reads, "value"),
|
|
18531
|
+
...companions
|
|
18532
|
+
}
|
|
18533
|
+
}, {
|
|
18534
|
+
kind: "fields",
|
|
18535
|
+
cap: "group-status",
|
|
18536
|
+
fields: {
|
|
18537
|
+
included: aggregateSource(template, spec, reads, "included"),
|
|
18538
|
+
excluded: aggregateSource(template, spec, reads, "excluded"),
|
|
18539
|
+
total: aggregateSource(template, spec, reads, "total")
|
|
18540
|
+
}
|
|
18541
|
+
}]
|
|
18542
|
+
};
|
|
18543
|
+
}
|
|
18544
|
+
function criterionProblems(template, spec) {
|
|
18545
|
+
if (template.family === "actuator") {
|
|
18546
|
+
if (spec.active === void 0) return [problem("kind-mismatch", "helper.active", `a ${template.id} group needs an active criterion (at-least-one or all)`)];
|
|
18547
|
+
const reducer = reducerForActive(spec.active);
|
|
18548
|
+
if (spec.aggregate !== reducer) return [problem("kind-mismatch", "helper.aggregate", `active \`${spec.active}\` aggregates by \`${reducer}\`, not \`${spec.aggregate}\``)];
|
|
18549
|
+
return [];
|
|
18550
|
+
}
|
|
18551
|
+
if (spec.active !== void 0) return [problem("kind-mismatch", "helper.active", `a ${template.id} group has no active criterion — that belongs to an actuator group`)];
|
|
18552
|
+
return [];
|
|
18553
|
+
}
|
|
18554
|
+
function parseDraft(draft) {
|
|
18555
|
+
const parsed = CompositionSchema.safeParse(draft);
|
|
18556
|
+
if (parsed.success) return {
|
|
18557
|
+
ok: true,
|
|
18558
|
+
composition: parsed.data
|
|
18559
|
+
};
|
|
18560
|
+
return {
|
|
18561
|
+
ok: false,
|
|
18562
|
+
problems: [problem("helper-mismatch", "helper", `the template did not compile: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`)]
|
|
18563
|
+
};
|
|
18564
|
+
}
|
|
18565
|
+
/** Compile a group template to a new-device composition. Refusals are named. */
|
|
18566
|
+
function compileHelper(spec) {
|
|
18567
|
+
const template = groupTemplate(spec.templateId);
|
|
18568
|
+
if (template === null) return {
|
|
18569
|
+
ok: false,
|
|
18570
|
+
problems: [problem("member-constraint", "helper.templateId", `unknown template \`${spec.templateId}\``)]
|
|
18571
|
+
};
|
|
18572
|
+
const criterion = criterionProblems(template, spec);
|
|
18573
|
+
if (criterion.length > 0) return {
|
|
18574
|
+
ok: false,
|
|
18575
|
+
problems: criterion
|
|
18576
|
+
};
|
|
18577
|
+
if (!template.aggregates.includes(spec.aggregate)) {
|
|
18578
|
+
const allowed = template.aggregates.join(", ");
|
|
18579
|
+
return {
|
|
18580
|
+
ok: false,
|
|
18581
|
+
problems: [problem("kind-mismatch", "helper.aggregate", `a ${template.id} group aggregates by ${allowed}, not \`${spec.aggregate}\``)]
|
|
18582
|
+
};
|
|
18583
|
+
}
|
|
18584
|
+
const members = memberProblems(template, spec);
|
|
18585
|
+
if (members.length > 0) return {
|
|
18586
|
+
ok: false,
|
|
18587
|
+
problems: members
|
|
18588
|
+
};
|
|
18589
|
+
return parseDraft(draftOf$1(template, spec));
|
|
18590
|
+
}
|
|
18591
|
+
/** The built body, ignoring `helper`, so a save can demand one writer. */
|
|
18592
|
+
function sameHelperBody(stored, compiled) {
|
|
18593
|
+
return JSON.stringify(stored.target) === JSON.stringify(compiled.target) && JSON.stringify(stored.features) === JSON.stringify(compiled.features);
|
|
18594
|
+
}
|
|
18595
|
+
function quantityProblems(spec, facts) {
|
|
18596
|
+
const template = groupTemplate(spec.templateId);
|
|
18597
|
+
if (template === null || template.family !== "numeric") return [];
|
|
18598
|
+
const known = spec.members.flatMap((member, index) => {
|
|
18599
|
+
const quantity = facts.get(compositionSourceKey(member.source))?.quantity;
|
|
18600
|
+
return quantity === void 0 ? [] : [{
|
|
18601
|
+
index,
|
|
18602
|
+
quantity
|
|
18603
|
+
}];
|
|
18604
|
+
});
|
|
18605
|
+
if (template.quantity !== "device") return known.flatMap((member) => member.quantity === template.quantity ? [] : [problem("mixed-quantity", `helper.members[${member.index}]`, `a ${template.id} group is ${template.quantity}${template.baseUnit === void 0 ? "" : ` (${template.baseUnit})`}; this member is ${member.quantity}`)]);
|
|
18606
|
+
const distinct = [...new Set(known.map((member) => member.quantity))];
|
|
18607
|
+
if (distinct.length <= 1) return [];
|
|
18608
|
+
return [problem("mixed-quantity", "helper.members", `a numeric group requires one quantity; members report ${distinct.join(" and ")}`)];
|
|
18609
|
+
}
|
|
18610
|
+
function deviceTypeProblems(spec, facts) {
|
|
18611
|
+
const template = groupTemplate(spec.templateId);
|
|
18612
|
+
if (template === null) return [];
|
|
18613
|
+
return spec.members.flatMap((member, index) => {
|
|
18614
|
+
const deviceType = facts.get(compositionSourceKey(member.source))?.deviceType;
|
|
18615
|
+
if (deviceType === void 0 || deviceType === template.memberDeviceType) return [];
|
|
18616
|
+
return [problem("member-constraint", `helper.members[${index}]`, `a ${template.id} group takes ${template.memberDeviceType} devices; this member is a ${deviceType}`)];
|
|
18617
|
+
});
|
|
18618
|
+
}
|
|
18619
|
+
/**
|
|
18620
|
+
* Problems that refuse a save. No `helper` key means a free composition: this
|
|
18621
|
+
* returns nothing, and expression semantics stay as they are.
|
|
18622
|
+
*/
|
|
18623
|
+
function helperSaveProblems(composition, facts) {
|
|
18624
|
+
const spec = composition.helper;
|
|
18625
|
+
if (spec === void 0) return [];
|
|
18626
|
+
const compiled = compileHelper(spec);
|
|
18627
|
+
if (!compiled.ok) return compiled.problems;
|
|
18628
|
+
if (!sameHelperBody(composition, compiled.composition)) return [problem("helper-mismatch", "helper", "the stored features are not this template — the helper spec is the only writer")];
|
|
18629
|
+
return [...quantityProblems(spec, facts), ...deviceTypeProblems(spec, facts)];
|
|
18630
|
+
}
|
|
18631
|
+
/**
|
|
17973
18632
|
* The field graph across every composition, and the cycles in it (D659).
|
|
17974
18633
|
*
|
|
17975
18634
|
* A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
|
|
@@ -18150,11 +18809,22 @@ function composableCap(feature, at, lookupCap) {
|
|
|
18150
18809
|
* required field. A REPLACE needs none of it: the cap is bound (a fact), the
|
|
18151
18810
|
* native keeps serving its methods, and only the named fields are owned (D663).
|
|
18152
18811
|
*/
|
|
18153
|
-
|
|
18812
|
+
/**
|
|
18813
|
+
* Caps an actuator helper is allowed to serve. A free composition, and a
|
|
18814
|
+
* reading group, get an empty set — `has-commands` stays for them.
|
|
18815
|
+
*/
|
|
18816
|
+
function actuatorCommandCaps(composition) {
|
|
18817
|
+
const spec = composition.helper;
|
|
18818
|
+
if (spec === void 0) return /* @__PURE__ */ new Set();
|
|
18819
|
+
const template = groupTemplate(spec.templateId);
|
|
18820
|
+
if (template === null || template.family !== "actuator") return /* @__PURE__ */ new Set();
|
|
18821
|
+
return new Set(template.commandCaps);
|
|
18822
|
+
}
|
|
18823
|
+
function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType, servedCommands) {
|
|
18154
18824
|
const problems = [];
|
|
18155
18825
|
if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) problems.push(problem("wrong-device-type", `${at}.cap`, `\`${def.name}\` is not offered on a \`${deviceType}\` device (it allows ${def.deviceTypes.join(", ")})`));
|
|
18156
18826
|
const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
|
|
18157
|
-
if (served.length > 0) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
|
|
18827
|
+
if (served.length > 0 && !servedCommands.has(def.name)) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
|
|
18158
18828
|
const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
|
|
18159
18829
|
for (const key of requiredTopLevelKeys(state)) {
|
|
18160
18830
|
if (key === itemArrayPath) continue;
|
|
@@ -18174,6 +18844,7 @@ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
|
|
|
18174
18844
|
const role = roleOf(source);
|
|
18175
18845
|
const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
|
|
18176
18846
|
if (!unavailable.ok) return refused(unavailable.problem);
|
|
18847
|
+
const omitEmpty = source.kind === "aggregate" && source.publish === "value" && source.empty === "omit";
|
|
18177
18848
|
const plan = {
|
|
18178
18849
|
cap: def.name,
|
|
18179
18850
|
path,
|
|
@@ -18181,7 +18852,7 @@ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
|
|
|
18181
18852
|
kind: tf.kind,
|
|
18182
18853
|
nullable: tf.nullable,
|
|
18183
18854
|
required: tf.required,
|
|
18184
|
-
unavailable: unavailable.unavailable,
|
|
18855
|
+
unavailable: omitEmpty ? "omit" : unavailable.unavailable,
|
|
18185
18856
|
role,
|
|
18186
18857
|
source,
|
|
18187
18858
|
reads: [...fieldReads(source)]
|
|
@@ -18194,11 +18865,11 @@ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
|
|
|
18194
18865
|
events: []
|
|
18195
18866
|
};
|
|
18196
18867
|
}
|
|
18197
|
-
function planFeature(feature, featureIndex, target, lookupCap) {
|
|
18868
|
+
function planFeature(feature, featureIndex, target, lookupCap, servedCommands) {
|
|
18198
18869
|
const at = `features[${featureIndex}]`;
|
|
18199
18870
|
const cap = composableCap(feature, at, lookupCap);
|
|
18200
18871
|
if ("code" in cap) return refused(cap);
|
|
18201
|
-
const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
|
|
18872
|
+
const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType, servedCommands)];
|
|
18202
18873
|
if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
|
|
18203
18874
|
const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
|
|
18204
18875
|
featureIndex,
|
|
@@ -18284,6 +18955,7 @@ function featureTargetOf(target, cap, at) {
|
|
|
18284
18955
|
function planComposition(composition, lookupCap, target) {
|
|
18285
18956
|
const planTarget = planTargetOf(composition, target);
|
|
18286
18957
|
if ("code" in planTarget) return refused(planTarget);
|
|
18958
|
+
const servedCommands = actuatorCommandCaps(composition);
|
|
18287
18959
|
const results = composition.features.map((feature, index) => {
|
|
18288
18960
|
const at = `features[${index}]`;
|
|
18289
18961
|
if (feature.kind === "passthrough") return refused(problem("reserved-variant", at, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`));
|
|
@@ -18298,9 +18970,9 @@ function planComposition(composition, lookupCap, target) {
|
|
|
18298
18970
|
return nativeReplaceOrBlock(featureTarget, planFeature(feature, index, {
|
|
18299
18971
|
mode: "add",
|
|
18300
18972
|
deviceType: planTarget.found.type
|
|
18301
|
-
}, lookupCap));
|
|
18973
|
+
}, lookupCap, servedCommands));
|
|
18302
18974
|
}
|
|
18303
|
-
const result = planFeature(feature, index, featureTarget, lookupCap);
|
|
18975
|
+
const result = planFeature(feature, index, featureTarget, lookupCap, servedCommands);
|
|
18304
18976
|
return compositionFeatureEnabled(feature) ? result : {
|
|
18305
18977
|
problems: result.problems,
|
|
18306
18978
|
fields: [],
|
|
@@ -18446,10 +19118,15 @@ function validateComposition(composition, ctx) {
|
|
|
18446
19118
|
if (refusedFeatures.some((at) => item.path === at || item.path.startsWith(`${at}.`))) return false;
|
|
18447
19119
|
return refusedFeatures.length === 0 || item.path !== "features";
|
|
18448
19120
|
});
|
|
19121
|
+
const facts = new Map([...ctx.sources].flatMap(([key, source]) => source.kind === "found" ? [[key, {
|
|
19122
|
+
deviceType: source.deviceType,
|
|
19123
|
+
quantity: source.quantity
|
|
19124
|
+
}]] : []));
|
|
18449
19125
|
const problems = [
|
|
18450
19126
|
...plan.problems,
|
|
18451
19127
|
...sourceProblems(plan.fields, ctx, self),
|
|
18452
|
-
...cycles
|
|
19128
|
+
...cycles,
|
|
19129
|
+
...helperSaveProblems(composition, facts)
|
|
18453
19130
|
];
|
|
18454
19131
|
return {
|
|
18455
19132
|
ok: problems.length === 0,
|
|
@@ -47747,6 +48424,8 @@ var NcSystemEventKindSchema = zod.z.enum([
|
|
|
47747
48424
|
"detection-blind",
|
|
47748
48425
|
"motion-noisy",
|
|
47749
48426
|
"motion-calm",
|
|
48427
|
+
"package-llm-silent",
|
|
48428
|
+
"package-llm-answered",
|
|
47750
48429
|
"recording-stalled",
|
|
47751
48430
|
"recording-healthy",
|
|
47752
48431
|
"recording-degraded",
|
|
@@ -61712,6 +62391,28 @@ var PackageVersionInfoSchema = zod.z.object({
|
|
|
61712
62391
|
deprecated: zod.z.string().optional(),
|
|
61713
62392
|
distTags: zod.z.array(zod.z.string()).readonly()
|
|
61714
62393
|
});
|
|
62394
|
+
/**
|
|
62395
|
+
* One published document. `ok` is markdown from the registry. `missing` means
|
|
62396
|
+
* the registry answered and the file was not there. `error` is a registry or
|
|
62397
|
+
* tarball failure — never an empty string standing in for either.
|
|
62398
|
+
*/
|
|
62399
|
+
var PackageDocBodySchema = zod.z.discriminatedUnion("status", [
|
|
62400
|
+
zod.z.object({
|
|
62401
|
+
status: zod.z.literal("ok"),
|
|
62402
|
+
markdown: zod.z.string()
|
|
62403
|
+
}),
|
|
62404
|
+
zod.z.object({ status: zod.z.literal("missing") }),
|
|
62405
|
+
zod.z.object({
|
|
62406
|
+
status: zod.z.literal("error"),
|
|
62407
|
+
message: zod.z.string()
|
|
62408
|
+
})
|
|
62409
|
+
]);
|
|
62410
|
+
var PackageDocsSchema = zod.z.object({
|
|
62411
|
+
name: zod.z.string(),
|
|
62412
|
+
version: zod.z.string(),
|
|
62413
|
+
readme: PackageDocBodySchema,
|
|
62414
|
+
changelog: PackageDocBodySchema
|
|
62415
|
+
});
|
|
61715
62416
|
var SearchResultSchema = zod.z.object({
|
|
61716
62417
|
name: zod.z.string(),
|
|
61717
62418
|
version: zod.z.string(),
|
|
@@ -62078,6 +62779,15 @@ var addonsCapability = {
|
|
|
62078
62779
|
auth: "admin"
|
|
62079
62780
|
}),
|
|
62080
62781
|
getVersions: method(zod.z.object({ name: zod.z.string() }), zod.z.array(PackageVersionInfoSchema).readonly()),
|
|
62782
|
+
/**
|
|
62783
|
+
* README and CHANGELOG for one registry package, read on the hub and
|
|
62784
|
+
* cached. `version` omitted resolves the `latest` dist-tag. The browser
|
|
62785
|
+
* never talks to the registry.
|
|
62786
|
+
*/
|
|
62787
|
+
getPackageDocs: method(zod.z.object({
|
|
62788
|
+
name: zod.z.string().min(1),
|
|
62789
|
+
version: zod.z.string().min(1).optional()
|
|
62790
|
+
}), PackageDocsSchema, { auth: "admin" }),
|
|
62081
62791
|
restartAddon: method(zod.z.object({ addonId: zod.z.string() }), RestartAddonResultSchema, {
|
|
62082
62792
|
kind: "mutation",
|
|
62083
62793
|
auth: "admin"
|
|
@@ -63575,6 +64285,10 @@ var ColorStatusSchema = zod.z.object({
|
|
|
63575
64285
|
/** Ms epoch of the last operator-driven change. */
|
|
63576
64286
|
lastChangedAt: zod.z.number()
|
|
63577
64287
|
});
|
|
64288
|
+
function parseColorInput(value) {
|
|
64289
|
+
const parsed = ColorInputSchema.safeParse(value);
|
|
64290
|
+
return parsed.success ? parsed.data : null;
|
|
64291
|
+
}
|
|
63578
64292
|
var colorCapability = {
|
|
63579
64293
|
name: "color",
|
|
63580
64294
|
scope: "device",
|
|
@@ -65651,6 +66365,95 @@ var gasCapability = {
|
|
|
65651
66365
|
* whether persisting is worth a SQLite commit. */
|
|
65652
66366
|
volatileStateFields: ["lastChangedAt"]
|
|
65653
66367
|
};
|
|
66368
|
+
/**
|
|
66369
|
+
* The last command a helper actuator group sent to each member (D944).
|
|
66370
|
+
* The composer writes this slice; the aggregate pass does not, so a later
|
|
66371
|
+
* reading does not wipe it. History is none — the outcome is the latest
|
|
66372
|
+
* attempt, not a series.
|
|
66373
|
+
*/
|
|
66374
|
+
var GroupCommandResultSchema = zod.z.object({
|
|
66375
|
+
deviceId: zod.z.number().int().nonnegative(),
|
|
66376
|
+
cap: zod.z.string().min(1),
|
|
66377
|
+
method: zod.z.string().min(1),
|
|
66378
|
+
ok: zod.z.boolean(),
|
|
66379
|
+
error: zod.z.string().optional(),
|
|
66380
|
+
/** Ms epoch of the attempt. */
|
|
66381
|
+
at: zod.z.number()
|
|
66382
|
+
});
|
|
66383
|
+
var GroupCommandsSchema = zod.z.object({
|
|
66384
|
+
/** One row per member that has been commanded. A later command replaces that member's row. */
|
|
66385
|
+
results: zod.z.array(GroupCommandResultSchema) });
|
|
66386
|
+
var groupCommandsCapability = {
|
|
66387
|
+
name: "group-commands",
|
|
66388
|
+
scope: "device",
|
|
66389
|
+
deviceNative: true,
|
|
66390
|
+
mode: "singleton",
|
|
66391
|
+
deviceTypes: [DeviceType.Switch, DeviceType.Light],
|
|
66392
|
+
methods: {},
|
|
66393
|
+
status: {
|
|
66394
|
+
schema: GroupCommandsSchema,
|
|
66395
|
+
kind: "push"
|
|
66396
|
+
},
|
|
66397
|
+
runtimeState: GroupCommandsSchema,
|
|
66398
|
+
/**
|
|
66399
|
+
* Runtime-state durability: **session** — the last attempt. A restored
|
|
66400
|
+
* failure would outlive the command that caused it.
|
|
66401
|
+
*
|
|
66402
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
66403
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
66404
|
+
*/
|
|
66405
|
+
durability: "session",
|
|
66406
|
+
history: "none"
|
|
66407
|
+
};
|
|
66408
|
+
/**
|
|
66409
|
+
* How many members of a helper group answered (D944). Counts are integers,
|
|
66410
|
+
* not measurements: zero included is a real count, never a stand-in reading.
|
|
66411
|
+
* The reading itself stays on the output capability. History is none — the
|
|
66412
|
+
* series that later statistics read is the output cap's own history block.
|
|
66413
|
+
*/
|
|
66414
|
+
var GroupStatusSchema = zod.z.object({
|
|
66415
|
+
/** Members whose current value entered the aggregate. */
|
|
66416
|
+
included: zod.z.number().int().nonnegative().meta(readingMeta("count", {
|
|
66417
|
+
precision: 0,
|
|
66418
|
+
label: "Reporting"
|
|
66419
|
+
})),
|
|
66420
|
+
/** Members left out: offline, unknown, or not a finite reading (D393). */
|
|
66421
|
+
excluded: zod.z.number().int().nonnegative().meta(readingMeta("count", {
|
|
66422
|
+
precision: 0,
|
|
66423
|
+
label: "Excluded"
|
|
66424
|
+
})),
|
|
66425
|
+
/** `included` + `excluded`. */
|
|
66426
|
+
total: zod.z.number().int().nonnegative().meta(readingMeta("count", {
|
|
66427
|
+
precision: 0,
|
|
66428
|
+
label: "Members"
|
|
66429
|
+
}))
|
|
66430
|
+
});
|
|
66431
|
+
var groupStatusCapability = {
|
|
66432
|
+
name: "group-status",
|
|
66433
|
+
scope: "device",
|
|
66434
|
+
deviceNative: true,
|
|
66435
|
+
mode: "singleton",
|
|
66436
|
+
deviceTypes: [
|
|
66437
|
+
DeviceType.Sensor,
|
|
66438
|
+
DeviceType.Switch,
|
|
66439
|
+
DeviceType.Light
|
|
66440
|
+
],
|
|
66441
|
+
methods: {},
|
|
66442
|
+
status: {
|
|
66443
|
+
schema: GroupStatusSchema,
|
|
66444
|
+
kind: "push"
|
|
66445
|
+
},
|
|
66446
|
+
runtimeState: GroupStatusSchema,
|
|
66447
|
+
/**
|
|
66448
|
+
* Runtime-state durability: **session** — recomputed from members on the
|
|
66449
|
+
* next source event; a restored count would outlive the readings it named.
|
|
66450
|
+
*
|
|
66451
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
66452
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
66453
|
+
*/
|
|
66454
|
+
durability: "session",
|
|
66455
|
+
history: "none"
|
|
66456
|
+
};
|
|
65654
66457
|
var HeatingValveStatusSchema = zod.z.object({
|
|
65655
66458
|
/** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
|
|
65656
66459
|
position: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
@@ -67715,12 +68518,18 @@ var motionCapability = {
|
|
|
67715
68518
|
methods: {
|
|
67716
68519
|
/**
|
|
67717
68520
|
* Pull the current motion state synchronously. Convenience shortcut
|
|
67718
|
-
* for consumers that don't need the full status object
|
|
67719
|
-
*
|
|
67720
|
-
* `getStatus` once the auto-injected status surface lands in every
|
|
67721
|
-
* consumer.
|
|
68521
|
+
* for consumers that don't need the full status object. A composed
|
|
68522
|
+
* group answers from its slice (D944): unknown throws, it is not off.
|
|
67722
68523
|
*/
|
|
67723
68524
|
isDetected: method(zod.z.object({ deviceId: zod.z.number() }), zod.z.boolean()) },
|
|
68525
|
+
/**
|
|
68526
|
+
* A composed motion group serves `isDetected` from `detected` (D944).
|
|
68527
|
+
* Without this the cap stays uncomposable (`has-commands`).
|
|
68528
|
+
*/
|
|
68529
|
+
composedMethods: { isDetected: {
|
|
68530
|
+
kind: "slice",
|
|
68531
|
+
field: "detected"
|
|
68532
|
+
} },
|
|
67724
68533
|
events: {
|
|
67725
68534
|
/**
|
|
67726
68535
|
* Fires every time the runner transitions a camera between
|
|
@@ -76628,6 +77437,103 @@ function createDeviceExtensionProvider(options) {
|
|
|
76628
77437
|
}
|
|
76629
77438
|
};
|
|
76630
77439
|
}
|
|
77440
|
+
function isBinaryReducer(reducer) {
|
|
77441
|
+
return reducer === "any" || reducer === "all";
|
|
77442
|
+
}
|
|
77443
|
+
function includedValue(reducer, member) {
|
|
77444
|
+
if (member.state !== "fresh") return null;
|
|
77445
|
+
if (isBinaryReducer(reducer)) return typeof member.value === "boolean" ? member.value : null;
|
|
77446
|
+
return typeof member.value === "number" && Number.isFinite(member.value) ? member.value : null;
|
|
77447
|
+
}
|
|
77448
|
+
function median$1(values) {
|
|
77449
|
+
const sorted = [...values].toSorted((a, b) => a - b);
|
|
77450
|
+
const mid = Math.floor(sorted.length / 2);
|
|
77451
|
+
const right = sorted[mid];
|
|
77452
|
+
const left = sorted[mid - 1];
|
|
77453
|
+
if (right === void 0) throw new Error("median requires at least one finite number");
|
|
77454
|
+
if (sorted.length % 2 === 1) return right;
|
|
77455
|
+
if (left === void 0) throw new Error("median requires at least one finite number");
|
|
77456
|
+
return (left + right) / 2;
|
|
77457
|
+
}
|
|
77458
|
+
function reduceNumbers(reducer, values) {
|
|
77459
|
+
if (reducer === "min") return Math.min(...values);
|
|
77460
|
+
if (reducer === "max") return Math.max(...values);
|
|
77461
|
+
if (reducer === "sum") return values.reduce((sum, value) => sum + value, 0);
|
|
77462
|
+
if (reducer === "median") return median$1(values);
|
|
77463
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
77464
|
+
}
|
|
77465
|
+
function reduceBooleans(reducer, values) {
|
|
77466
|
+
return reducer === "all" ? values.every((value) => value) : values.some((value) => value);
|
|
77467
|
+
}
|
|
77468
|
+
function countsOf(included, total) {
|
|
77469
|
+
return {
|
|
77470
|
+
included,
|
|
77471
|
+
excluded: total - included,
|
|
77472
|
+
total
|
|
77473
|
+
};
|
|
77474
|
+
}
|
|
77475
|
+
function fatalReason(members) {
|
|
77476
|
+
const reasons = members.flatMap((member) => member.state === "unavailable" && member.fatal === true ? [member.reason ?? "source gone"] : []);
|
|
77477
|
+
return reasons.length === 0 ? null : reasons.join("; ");
|
|
77478
|
+
}
|
|
77479
|
+
function emptyOutcome(empty, counts) {
|
|
77480
|
+
if (empty === "null") return {
|
|
77481
|
+
kind: "value",
|
|
77482
|
+
value: null,
|
|
77483
|
+
counts
|
|
77484
|
+
};
|
|
77485
|
+
return {
|
|
77486
|
+
kind: "unavailable",
|
|
77487
|
+
fatal: false,
|
|
77488
|
+
reason: "no member included — unknown is not 0 or off",
|
|
77489
|
+
counts
|
|
77490
|
+
};
|
|
77491
|
+
}
|
|
77492
|
+
/** Pure. `members` is not mutated. An empty included set is null or unavailable, never 0 or false. */
|
|
77493
|
+
function evaluateAggregate(input) {
|
|
77494
|
+
const included = input.members.flatMap((member) => {
|
|
77495
|
+
const value = includedValue(input.reducer, member);
|
|
77496
|
+
return value === null ? [] : [value];
|
|
77497
|
+
});
|
|
77498
|
+
const counts = countsOf(included.length, input.members.length);
|
|
77499
|
+
const gone = fatalReason(input.members);
|
|
77500
|
+
if (gone !== null) return {
|
|
77501
|
+
kind: "unavailable",
|
|
77502
|
+
fatal: true,
|
|
77503
|
+
reason: gone,
|
|
77504
|
+
counts
|
|
77505
|
+
};
|
|
77506
|
+
if (input.publish === "included") return {
|
|
77507
|
+
kind: "value",
|
|
77508
|
+
value: counts.included,
|
|
77509
|
+
counts
|
|
77510
|
+
};
|
|
77511
|
+
if (input.publish === "excluded") return {
|
|
77512
|
+
kind: "value",
|
|
77513
|
+
value: counts.excluded,
|
|
77514
|
+
counts
|
|
77515
|
+
};
|
|
77516
|
+
if (input.publish === "total") return {
|
|
77517
|
+
kind: "value",
|
|
77518
|
+
value: counts.total,
|
|
77519
|
+
counts
|
|
77520
|
+
};
|
|
77521
|
+
if (included.length === 0) return emptyOutcome(input.empty, counts);
|
|
77522
|
+
if (isBinaryReducer(input.reducer)) {
|
|
77523
|
+
const flags = included.filter((value) => typeof value === "boolean");
|
|
77524
|
+
return {
|
|
77525
|
+
kind: "value",
|
|
77526
|
+
value: reduceBooleans(input.reducer, flags),
|
|
77527
|
+
counts
|
|
77528
|
+
};
|
|
77529
|
+
}
|
|
77530
|
+
const numbers = included.filter((value) => typeof value === "number");
|
|
77531
|
+
return {
|
|
77532
|
+
kind: "value",
|
|
77533
|
+
value: reduceNumbers(input.reducer, numbers),
|
|
77534
|
+
counts
|
|
77535
|
+
};
|
|
77536
|
+
}
|
|
76631
77537
|
/**
|
|
76632
77538
|
* Frozen unit → spec table. `•` in the doc marks each dimension's canonical
|
|
76633
77539
|
* unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
|
|
@@ -77133,11 +78039,45 @@ function expressionOutcome(source, read, now, stateful) {
|
|
|
77133
78039
|
deadline: deadlines.length > 0 ? Math.min(...deadlines) : null
|
|
77134
78040
|
};
|
|
77135
78041
|
}
|
|
78042
|
+
function aggregateRead(reading) {
|
|
78043
|
+
if (reading.state === "unavailable") return {
|
|
78044
|
+
state: "unavailable",
|
|
78045
|
+
fatal: reading.fatal,
|
|
78046
|
+
reason: reading.reason
|
|
78047
|
+
};
|
|
78048
|
+
return {
|
|
78049
|
+
state: reading.state,
|
|
78050
|
+
value: reading.value,
|
|
78051
|
+
...reading.state === "stale" ? { reason: reading.reason } : {}
|
|
78052
|
+
};
|
|
78053
|
+
}
|
|
78054
|
+
function aggregateField(source, read) {
|
|
78055
|
+
const outcome = evaluateAggregate({
|
|
78056
|
+
reducer: source.reducer,
|
|
78057
|
+
members: source.reads.map((item) => aggregateRead(read(item))),
|
|
78058
|
+
publish: source.publish,
|
|
78059
|
+
empty: source.empty
|
|
78060
|
+
});
|
|
78061
|
+
if (outcome.kind === "value") return {
|
|
78062
|
+
state: "fresh",
|
|
78063
|
+
value: outcome.value,
|
|
78064
|
+
reason: null,
|
|
78065
|
+
deadline: null
|
|
78066
|
+
};
|
|
78067
|
+
return {
|
|
78068
|
+
state: "unavailable",
|
|
78069
|
+
value: null,
|
|
78070
|
+
reason: outcome.reason,
|
|
78071
|
+
fatal: outcome.fatal,
|
|
78072
|
+
confirmed: true
|
|
78073
|
+
};
|
|
78074
|
+
}
|
|
77136
78075
|
function evaluateCompositionField(input) {
|
|
77137
78076
|
const { source } = input;
|
|
77138
78077
|
switch (source.kind) {
|
|
77139
78078
|
case "from": return fromReading(input.read(source), source);
|
|
77140
78079
|
case "expression": return expressionOutcome(source, input.read, input.now, input.stateful);
|
|
78080
|
+
case "aggregate": return aggregateField(source, input.read);
|
|
77141
78081
|
case "code": return {
|
|
77142
78082
|
state: "unavailable",
|
|
77143
78083
|
value: null,
|
|
@@ -77484,6 +78424,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
77484
78424
|
featureProbe: featureProbeCapability,
|
|
77485
78425
|
flood: floodCapability,
|
|
77486
78426
|
gas: gasCapability,
|
|
78427
|
+
groupCommands: groupCommandsCapability,
|
|
78428
|
+
groupStatus: groupStatusCapability,
|
|
77487
78429
|
heatingValve: heatingValveCapability,
|
|
77488
78430
|
humidifier: humidifierCapability,
|
|
77489
78431
|
humiditySensor: humiditySensorCapability,
|
|
@@ -78638,6 +79580,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
78638
79580
|
access: "view",
|
|
78639
79581
|
role: "admin"
|
|
78640
79582
|
},
|
|
79583
|
+
"addons.getPackageDocs": {
|
|
79584
|
+
capName: "addons",
|
|
79585
|
+
capScope: "system",
|
|
79586
|
+
addonId: null,
|
|
79587
|
+
access: "view",
|
|
79588
|
+
role: "admin"
|
|
79589
|
+
},
|
|
78641
79590
|
"addons.getVersions": {
|
|
78642
79591
|
capName: "addons",
|
|
78643
79592
|
capScope: "system",
|
|
@@ -81746,6 +82695,20 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
81746
82695
|
access: "view",
|
|
81747
82696
|
role: "viewer"
|
|
81748
82697
|
},
|
|
82698
|
+
"groupCommands.getStatus": {
|
|
82699
|
+
capName: "group-commands",
|
|
82700
|
+
capScope: "device",
|
|
82701
|
+
addonId: null,
|
|
82702
|
+
access: "view",
|
|
82703
|
+
role: "viewer"
|
|
82704
|
+
},
|
|
82705
|
+
"groupStatus.getStatus": {
|
|
82706
|
+
capName: "group-status",
|
|
82707
|
+
capScope: "device",
|
|
82708
|
+
addonId: null,
|
|
82709
|
+
access: "view",
|
|
82710
|
+
role: "viewer"
|
|
82711
|
+
},
|
|
81749
82712
|
"heatingValve.getStatus": {
|
|
81750
82713
|
capName: "heating-valve",
|
|
81751
82714
|
capScope: "device",
|
|
@@ -91568,6 +92531,37 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
91568
92531
|
icon: "flame"
|
|
91569
92532
|
}]
|
|
91570
92533
|
},
|
|
92534
|
+
"group-commands": {
|
|
92535
|
+
scope: "device",
|
|
92536
|
+
noun: t("Group command", "Comando di gruppo")
|
|
92537
|
+
},
|
|
92538
|
+
"group-status": {
|
|
92539
|
+
scope: "device",
|
|
92540
|
+
noun: t("Group", "Gruppo"),
|
|
92541
|
+
state: [
|
|
92542
|
+
{
|
|
92543
|
+
kind: "numeric",
|
|
92544
|
+
field: "included",
|
|
92545
|
+
quantity: t("the members reporting", "i membri che rispondono"),
|
|
92546
|
+
label: t("Reporting", "Rispondono"),
|
|
92547
|
+
icon: "users"
|
|
92548
|
+
},
|
|
92549
|
+
{
|
|
92550
|
+
kind: "numeric",
|
|
92551
|
+
field: "excluded",
|
|
92552
|
+
quantity: t("the members left out", "i membri esclusi"),
|
|
92553
|
+
label: t("Excluded", "Esclusi"),
|
|
92554
|
+
icon: "users"
|
|
92555
|
+
},
|
|
92556
|
+
{
|
|
92557
|
+
kind: "numeric",
|
|
92558
|
+
field: "total",
|
|
92559
|
+
quantity: t("the member count", "il numero di membri"),
|
|
92560
|
+
label: t("Members", "Membri"),
|
|
92561
|
+
icon: "sigma"
|
|
92562
|
+
}
|
|
92563
|
+
]
|
|
92564
|
+
},
|
|
91571
92565
|
"humidity-sensor": {
|
|
91572
92566
|
scope: "device",
|
|
91573
92567
|
noun: t("Humidity", "Umidità"),
|
|
@@ -92104,6 +93098,8 @@ var CAP_NAMES_WITH_STATUS = [
|
|
|
92104
93098
|
"feature-probe",
|
|
92105
93099
|
"flood",
|
|
92106
93100
|
"gas",
|
|
93101
|
+
"group-commands",
|
|
93102
|
+
"group-status",
|
|
92107
93103
|
"heating-valve",
|
|
92108
93104
|
"humidifier",
|
|
92109
93105
|
"humidity-sensor",
|
|
@@ -92233,6 +93229,8 @@ var ALL_CAPABILITY_DEFINITIONS = [
|
|
|
92233
93229
|
filesystemBrowseCapability,
|
|
92234
93230
|
floodCapability,
|
|
92235
93231
|
gasCapability,
|
|
93232
|
+
groupCommandsCapability,
|
|
93233
|
+
groupStatusCapability,
|
|
92236
93234
|
heatingValveCapability,
|
|
92237
93235
|
humidifierCapability,
|
|
92238
93236
|
humiditySensorCapability,
|
|
@@ -93307,6 +94305,16 @@ Object.freeze({
|
|
|
93307
94305
|
form: "single",
|
|
93308
94306
|
optional: false
|
|
93309
94307
|
}],
|
|
94308
|
+
"groupCommands.getStatus": [{
|
|
94309
|
+
name: "deviceId",
|
|
94310
|
+
form: "single",
|
|
94311
|
+
optional: false
|
|
94312
|
+
}],
|
|
94313
|
+
"groupStatus.getStatus": [{
|
|
94314
|
+
name: "deviceId",
|
|
94315
|
+
form: "single",
|
|
94316
|
+
optional: false
|
|
94317
|
+
}],
|
|
93310
94318
|
"heatingValve.getStatus": [{
|
|
93311
94319
|
name: "deviceId",
|
|
93312
94320
|
form: "single",
|
|
@@ -95116,7 +96124,7 @@ Object.freeze({
|
|
|
95116
96124
|
});
|
|
95117
96125
|
/**
|
|
95118
96126
|
* Per-cap runtime-state policy, projected from every `*.cap.ts` that declares
|
|
95119
|
-
* `runtimeState`.
|
|
96127
|
+
* `runtimeState`. 74 caps: 38 `restored`, 36 `session`.
|
|
95120
96128
|
*
|
|
95121
96129
|
* A cap absent from this map has no runtime-state slice at all. A cap PRESENT
|
|
95122
96130
|
* with `durability: 'session'` has one and has decided not to keep it.
|
|
@@ -95234,6 +96242,14 @@ var RUNTIME_STATE_POLICY = {
|
|
|
95234
96242
|
durability: "restored",
|
|
95235
96243
|
volatileFields: ["lastChangedAt"]
|
|
95236
96244
|
},
|
|
96245
|
+
"group-commands": {
|
|
96246
|
+
durability: "session",
|
|
96247
|
+
volatileFields: []
|
|
96248
|
+
},
|
|
96249
|
+
"group-status": {
|
|
96250
|
+
durability: "session",
|
|
96251
|
+
volatileFields: []
|
|
96252
|
+
},
|
|
95237
96253
|
"heating-valve": {
|
|
95238
96254
|
durability: "session",
|
|
95239
96255
|
volatileFields: ["lastFetchedAt"]
|
|
@@ -95428,7 +96444,7 @@ var SESSION_ONLY_POLICY = {
|
|
|
95428
96444
|
};
|
|
95429
96445
|
/**
|
|
95430
96446
|
* Cap history catalog, projected from every `*.cap.ts` `history` block.
|
|
95431
|
-
* 41 caps with series (75 series),
|
|
96447
|
+
* 41 caps with series (75 series), 32 caps answered `'none'`.
|
|
95432
96448
|
* Field keys are `runtimeState` paths (`a.b` into fixed objects only).
|
|
95433
96449
|
*/
|
|
95434
96450
|
var HISTORY_CATALOG = {
|
|
@@ -96000,6 +97016,8 @@ new Map(Object.entries({
|
|
|
96000
97016
|
"detection-blind": "device-events",
|
|
96001
97017
|
"motion-noisy": "device-events",
|
|
96002
97018
|
"motion-calm": "device-events",
|
|
97019
|
+
"package-llm-silent": "device-events",
|
|
97020
|
+
"package-llm-answered": "device-events",
|
|
96003
97021
|
"recording-stalled": "device-events",
|
|
96004
97022
|
"recording-healthy": "device-events",
|
|
96005
97023
|
"recording-degraded": "device-events",
|
|
@@ -96083,6 +97101,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
96083
97101
|
label: "Motion without a subject",
|
|
96084
97102
|
kinds: ["motion-noisy", "motion-calm"]
|
|
96085
97103
|
},
|
|
97104
|
+
{
|
|
97105
|
+
id: "package-llm",
|
|
97106
|
+
family: "device-events",
|
|
97107
|
+
label: "Package confirm",
|
|
97108
|
+
kinds: ["package-llm-silent", "package-llm-answered"]
|
|
97109
|
+
},
|
|
96086
97110
|
{
|
|
96087
97111
|
id: "recording",
|
|
96088
97112
|
family: "device-events",
|
|
@@ -96306,6 +97330,8 @@ var DEVICE_EVENTS = [
|
|
|
96306
97330
|
"detection-blind",
|
|
96307
97331
|
"motion-noisy",
|
|
96308
97332
|
"motion-calm",
|
|
97333
|
+
"package-llm-silent",
|
|
97334
|
+
"package-llm-answered",
|
|
96309
97335
|
"recording-stalled",
|
|
96310
97336
|
"recording-healthy",
|
|
96311
97337
|
"recording-degraded",
|
|
@@ -96574,6 +97600,15 @@ new Map([
|
|
|
96574
97600
|
snapshotLabel: "Cameras with constant motion now",
|
|
96575
97601
|
snapshotSection: null
|
|
96576
97602
|
},
|
|
97603
|
+
{
|
|
97604
|
+
id: "package-llm-confirm",
|
|
97605
|
+
family: "device-events",
|
|
97606
|
+
label: "Package confirm (not answering / answering again)",
|
|
97607
|
+
bad: ["package-llm-silent"],
|
|
97608
|
+
recovery: ["package-llm-answered"],
|
|
97609
|
+
snapshotLabel: "Cameras whose package confirm is not answering",
|
|
97610
|
+
snapshotSection: null
|
|
97611
|
+
},
|
|
96577
97612
|
{
|
|
96578
97613
|
id: "recording-health",
|
|
96579
97614
|
family: "device-events",
|
|
@@ -97859,6 +98894,12 @@ Object.defineProperty(exports, "ForeignSecretMarkerError", {
|
|
|
97859
98894
|
return ForeignSecretMarkerError;
|
|
97860
98895
|
}
|
|
97861
98896
|
});
|
|
98897
|
+
Object.defineProperty(exports, "GroupCommandResultSchema", {
|
|
98898
|
+
enumerable: true,
|
|
98899
|
+
get: function() {
|
|
98900
|
+
return GroupCommandResultSchema;
|
|
98901
|
+
}
|
|
98902
|
+
});
|
|
97862
98903
|
Object.defineProperty(exports, "HISTORY_CATALOG", {
|
|
97863
98904
|
enumerable: true,
|
|
97864
98905
|
get: function() {
|
|
@@ -98291,6 +99332,12 @@ Object.defineProperty(exports, "batteryCapability", {
|
|
|
98291
99332
|
return batteryCapability;
|
|
98292
99333
|
}
|
|
98293
99334
|
});
|
|
99335
|
+
Object.defineProperty(exports, "brightnessCapability", {
|
|
99336
|
+
enumerable: true,
|
|
99337
|
+
get: function() {
|
|
99338
|
+
return brightnessCapability;
|
|
99339
|
+
}
|
|
99340
|
+
});
|
|
98294
99341
|
Object.defineProperty(exports, "buildCapabilityLookup", {
|
|
98295
99342
|
enumerable: true,
|
|
98296
99343
|
get: function() {
|
|
@@ -98381,6 +99428,12 @@ Object.defineProperty(exports, "collectZodSecretPaths", {
|
|
|
98381
99428
|
return collectZodSecretPaths;
|
|
98382
99429
|
}
|
|
98383
99430
|
});
|
|
99431
|
+
Object.defineProperty(exports, "colorCapability", {
|
|
99432
|
+
enumerable: true,
|
|
99433
|
+
get: function() {
|
|
99434
|
+
return colorCapability;
|
|
99435
|
+
}
|
|
99436
|
+
});
|
|
98384
99437
|
Object.defineProperty(exports, "composedDeviceRef", {
|
|
98385
99438
|
enumerable: true,
|
|
98386
99439
|
get: function() {
|
|
@@ -98663,6 +99716,12 @@ Object.defineProperty(exports, "extractNestedAddonId", {
|
|
|
98663
99716
|
return extractNestedAddonId;
|
|
98664
99717
|
}
|
|
98665
99718
|
});
|
|
99719
|
+
Object.defineProperty(exports, "fanOutTargets", {
|
|
99720
|
+
enumerable: true,
|
|
99721
|
+
get: function() {
|
|
99722
|
+
return fanOutTargets;
|
|
99723
|
+
}
|
|
99724
|
+
});
|
|
98666
99725
|
Object.defineProperty(exports, "filesystemBrowseCapability", {
|
|
98667
99726
|
enumerable: true,
|
|
98668
99727
|
get: function() {
|
|
@@ -98699,6 +99758,18 @@ Object.defineProperty(exports, "gbToBytes", {
|
|
|
98699
99758
|
return gbToBytes;
|
|
98700
99759
|
}
|
|
98701
99760
|
});
|
|
99761
|
+
Object.defineProperty(exports, "groupCommandsCapability", {
|
|
99762
|
+
enumerable: true,
|
|
99763
|
+
get: function() {
|
|
99764
|
+
return groupCommandsCapability;
|
|
99765
|
+
}
|
|
99766
|
+
});
|
|
99767
|
+
Object.defineProperty(exports, "groupTemplate", {
|
|
99768
|
+
enumerable: true,
|
|
99769
|
+
get: function() {
|
|
99770
|
+
return groupTemplate;
|
|
99771
|
+
}
|
|
99772
|
+
});
|
|
98702
99773
|
Object.defineProperty(exports, "historyItemFieldPath", {
|
|
98703
99774
|
enumerable: true,
|
|
98704
99775
|
get: function() {
|
|
@@ -98993,6 +100064,12 @@ Object.defineProperty(exports, "parseAcceleratorFactsJson", {
|
|
|
98993
100064
|
return parseAcceleratorFactsJson;
|
|
98994
100065
|
}
|
|
98995
100066
|
});
|
|
100067
|
+
Object.defineProperty(exports, "parseColorInput", {
|
|
100068
|
+
enumerable: true,
|
|
100069
|
+
get: function() {
|
|
100070
|
+
return parseColorInput;
|
|
100071
|
+
}
|
|
100072
|
+
});
|
|
98996
100073
|
Object.defineProperty(exports, "parseJsonObject", {
|
|
98997
100074
|
enumerable: true,
|
|
98998
100075
|
get: function() {
|
|
@@ -99149,6 +100226,12 @@ Object.defineProperty(exports, "resolveSecretMarkers", {
|
|
|
99149
100226
|
return resolveSecretMarkers;
|
|
99150
100227
|
}
|
|
99151
100228
|
});
|
|
100229
|
+
Object.defineProperty(exports, "runFanOut", {
|
|
100230
|
+
enumerable: true,
|
|
100231
|
+
get: function() {
|
|
100232
|
+
return runFanOut;
|
|
100233
|
+
}
|
|
100234
|
+
});
|
|
99152
100235
|
Object.defineProperty(exports, "runtimeStatePolicyFor", {
|
|
99153
100236
|
enumerable: true,
|
|
99154
100237
|
get: function() {
|
|
@@ -99197,6 +100280,18 @@ Object.defineProperty(exports, "settingsStoreCapability", {
|
|
|
99197
100280
|
return settingsStoreCapability;
|
|
99198
100281
|
}
|
|
99199
100282
|
});
|
|
100283
|
+
Object.defineProperty(exports, "sharedColorModes", {
|
|
100284
|
+
enumerable: true,
|
|
100285
|
+
get: function() {
|
|
100286
|
+
return sharedColorModes;
|
|
100287
|
+
}
|
|
100288
|
+
});
|
|
100289
|
+
Object.defineProperty(exports, "sharedLightCaps", {
|
|
100290
|
+
enumerable: true,
|
|
100291
|
+
get: function() {
|
|
100292
|
+
return sharedLightCaps;
|
|
100293
|
+
}
|
|
100294
|
+
});
|
|
99200
100295
|
Object.defineProperty(exports, "shortHostname", {
|
|
99201
100296
|
enumerable: true,
|
|
99202
100297
|
get: function() {
|
|
@@ -99275,6 +100370,12 @@ Object.defineProperty(exports, "supportedRuntimes", {
|
|
|
99275
100370
|
return supportedRuntimes;
|
|
99276
100371
|
}
|
|
99277
100372
|
});
|
|
100373
|
+
Object.defineProperty(exports, "switchCapability", {
|
|
100374
|
+
enumerable: true,
|
|
100375
|
+
get: function() {
|
|
100376
|
+
return switchCapability;
|
|
100377
|
+
}
|
|
100378
|
+
});
|
|
99278
100379
|
Object.defineProperty(exports, "toExpressionValue", {
|
|
99279
100380
|
enumerable: true,
|
|
99280
100381
|
get: function() {
|