@camstack/system 1.2.485 → 1.2.486
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-_EXvi6F8.mjs → agent-https-C4PLR74Z.mjs} +1 -1
- package/dist/{agent-https-DBP7P-ee.js → agent-https-DP-Ds2hU.js} +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/composer.addon.js +59 -13
- package/dist/builtins/composer/composer.addon.mjs +59 -13
- 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 +3 -3
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BSQneZV9.js → composition-sources-CN8K2Q91.js} +4 -2
- package/dist/{composition-sources-BnZpIA8u.mjs → composition-sources-IxiUkrz1.mjs} +4 -2
- package/dist/{constants-CQfTeCnB.js → constants-1S7BBGMb.js} +1 -1
- package/dist/{constants-7D7xjw0h.mjs → constants-DLyGFHZ2.mjs} +1 -1
- package/dist/{device-ca-Bzd6UO_c.mjs → device-ca-Cbf7_R9t.mjs} +1 -1
- package/dist/{device-ca-D_4dI5gv.js → device-ca-DoOFG0nm.js} +1 -1
- package/dist/{dist-xCNJAQMh.js → dist-D-d_UWL4.js} +760 -19
- package/dist/{dist-Df6c0RX_.mjs → dist-HBue971z.mjs} +760 -19
- 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-CuEWb6gB.mjs} +2 -2
- package/dist/{hub-mdns-ciao-DRfmLR_Z.js → hub-mdns-ciao-dcacrwqd.js} +2 -2
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/{restore-archive-Co70oRya.mjs → restore-archive-BGBRadFJ.mjs} +1 -1
- package/dist/{restore-archive-DA7N6ZZA.js → restore-archive-DHdXrKwG.js} +1 -1
- package/dist/{retention-DfCW-dZS.mjs → retention-8uqDWll8.mjs} +1 -1
- package/dist/{retention-DNDOPW7Z.js → retention-ChgrkTfz.js} +1 -1
- package/dist/{wal-maintenance-CA0L6WZ9.js → wal-maintenance-BOT7wWvH.js} +0 -0
- package/dist/{wal-maintenance-71-j48Ci.mjs → wal-maintenance-DubGwqz2.mjs} +0 -0
- package/package.json +1 -1
|
@@ -765,7 +765,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
765
765
|
return EventCategory;
|
|
766
766
|
}({});
|
|
767
767
|
//#endregion
|
|
768
|
-
//#region ../types/dist/sleep-
|
|
768
|
+
//#region ../types/dist/sleep-B81gwQuR.mjs
|
|
769
769
|
/**
|
|
770
770
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
771
771
|
* window to float samples (D455).
|
|
@@ -5183,6 +5183,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
5183
5183
|
"filesystem-browse",
|
|
5184
5184
|
"flood",
|
|
5185
5185
|
"gas",
|
|
5186
|
+
"group-status",
|
|
5186
5187
|
"heating-valve",
|
|
5187
5188
|
"humidifier",
|
|
5188
5189
|
"humidity-sensor",
|
|
@@ -6001,6 +6002,20 @@ Object.freeze({
|
|
|
6001
6002
|
"path": "detected",
|
|
6002
6003
|
"valueType": "boolean"
|
|
6003
6004
|
}],
|
|
6005
|
+
"group-status": [
|
|
6006
|
+
{
|
|
6007
|
+
"path": "included",
|
|
6008
|
+
"valueType": "number"
|
|
6009
|
+
},
|
|
6010
|
+
{
|
|
6011
|
+
"path": "excluded",
|
|
6012
|
+
"valueType": "number"
|
|
6013
|
+
},
|
|
6014
|
+
{
|
|
6015
|
+
"path": "total",
|
|
6016
|
+
"valueType": "number"
|
|
6017
|
+
}
|
|
6018
|
+
],
|
|
6004
6019
|
"heating-valve": [{
|
|
6005
6020
|
"path": "position",
|
|
6006
6021
|
"valueType": "number"
|
|
@@ -7468,7 +7483,7 @@ function errMsg(err) {
|
|
|
7468
7483
|
return String(err);
|
|
7469
7484
|
}
|
|
7470
7485
|
//#endregion
|
|
7471
|
-
//#region ../types/dist/composition-
|
|
7486
|
+
//#region ../types/dist/composition-B85l9j1M.mjs
|
|
7472
7487
|
var AUDIO_ENCODER_BY_CODEC = {
|
|
7473
7488
|
opus: "libopus",
|
|
7474
7489
|
aac: "aac",
|
|
@@ -8986,6 +9001,68 @@ function evaluateExpressionSource(expr, bindingValues, now, opts) {
|
|
|
8986
9001
|
}
|
|
8987
9002
|
}
|
|
8988
9003
|
/**
|
|
9004
|
+
* A composition source identity: `(addonId, stableId)`, never a numeric device
|
|
9005
|
+
* id. Split out so helper schemas can name it without a cycle through
|
|
9006
|
+
* `composition.ts` (D659).
|
|
9007
|
+
*/
|
|
9008
|
+
var CompositionSourceRefSchema = z.object({
|
|
9009
|
+
/** The owning addon; `stableId` is unique only within it. */
|
|
9010
|
+
addonId: z.string().min(1),
|
|
9011
|
+
stableId: z.string().min(1)
|
|
9012
|
+
});
|
|
9013
|
+
var GROUP_TEMPLATE_IDS = [
|
|
9014
|
+
"temperature",
|
|
9015
|
+
"humidity",
|
|
9016
|
+
"pressure",
|
|
9017
|
+
"illuminance",
|
|
9018
|
+
"power",
|
|
9019
|
+
"energy",
|
|
9020
|
+
"battery",
|
|
9021
|
+
"numeric",
|
|
9022
|
+
"motion",
|
|
9023
|
+
"contact",
|
|
9024
|
+
"leak",
|
|
9025
|
+
"smoke",
|
|
9026
|
+
"occupancy",
|
|
9027
|
+
"binary"
|
|
9028
|
+
];
|
|
9029
|
+
var NUMERIC_AGGREGATES = [
|
|
9030
|
+
"mean",
|
|
9031
|
+
"median",
|
|
9032
|
+
"min",
|
|
9033
|
+
"max",
|
|
9034
|
+
"sum"
|
|
9035
|
+
];
|
|
9036
|
+
var BINARY_AGGREGATES = ["any", "all"];
|
|
9037
|
+
var AGGREGATE_REDUCERS = [
|
|
9038
|
+
"mean",
|
|
9039
|
+
"median",
|
|
9040
|
+
"min",
|
|
9041
|
+
"max",
|
|
9042
|
+
"sum",
|
|
9043
|
+
"any",
|
|
9044
|
+
"all"
|
|
9045
|
+
];
|
|
9046
|
+
var GroupTemplateIdSchema = z.enum(GROUP_TEMPLATE_IDS);
|
|
9047
|
+
var AggregateReducerSchema = z.enum(AGGREGATE_REDUCERS);
|
|
9048
|
+
/** One member. The field is stored so occupancy can name motion or binary. */
|
|
9049
|
+
var HelperMemberSchema = z.object({
|
|
9050
|
+
source: CompositionSourceRefSchema,
|
|
9051
|
+
cap: z.string().min(1),
|
|
9052
|
+
fieldPath: z.string().min(1)
|
|
9053
|
+
});
|
|
9054
|
+
/**
|
|
9055
|
+
* Phase 1a is groups only. Math, integral and statistics stay off this union
|
|
9056
|
+
* until the composer can run them — a control must not save a spec it cannot.
|
|
9057
|
+
*/
|
|
9058
|
+
var GroupHelperSpecSchema = z.object({
|
|
9059
|
+
kind: z.literal("group"),
|
|
9060
|
+
templateId: GroupTemplateIdSchema,
|
|
9061
|
+
aggregate: AggregateReducerSchema,
|
|
9062
|
+
members: z.array(HelperMemberSchema).min(1).max(64)
|
|
9063
|
+
});
|
|
9064
|
+
var HelperSpecSchema = z.discriminatedUnion("kind", [GroupHelperSpecSchema]);
|
|
9065
|
+
/**
|
|
8989
9066
|
* A COMPOSITION: a device assembled from other devices' state (D659).
|
|
8990
9067
|
*
|
|
8991
9068
|
* The data half of the composed-devices spec §1. A composition is stored inside
|
|
@@ -9010,11 +9087,6 @@ var COMPOSER_ADDON_ID = "composer";
|
|
|
9010
9087
|
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
9011
9088
|
/** Longest snippet a `code` source may carry (slice 4). */
|
|
9012
9089
|
var MAX_COMPOSITION_CODE_LENGTH = 2e4;
|
|
9013
|
-
var CompositionSourceRefSchema = z.object({
|
|
9014
|
-
/** The owning addon; `stableId` is unique only within it. */
|
|
9015
|
-
addonId: z.string().min(1),
|
|
9016
|
-
stableId: z.string().min(1)
|
|
9017
|
-
});
|
|
9018
9090
|
/** One field of one capability of one source device. */
|
|
9019
9091
|
var CompositionFieldReadSchema = z.object({
|
|
9020
9092
|
source: CompositionSourceRefSchema,
|
|
@@ -9057,10 +9129,42 @@ var CompositionCodeSourceSchema = z.object({
|
|
|
9057
9129
|
kind: z.literal("code"),
|
|
9058
9130
|
code: z.string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
|
|
9059
9131
|
});
|
|
9132
|
+
/**
|
|
9133
|
+
* `aggregate`: a group reducer. Offline and unknown reads are excluded, not
|
|
9134
|
+
* folded in as 0 or false (D944). Free expressions stay `expression` so a
|
|
9135
|
+
* stale member is still included there.
|
|
9136
|
+
*/
|
|
9137
|
+
var CompositionAggregateSourceSchema = z.object({
|
|
9138
|
+
kind: z.literal("aggregate"),
|
|
9139
|
+
reducer: z.enum([
|
|
9140
|
+
"mean",
|
|
9141
|
+
"median",
|
|
9142
|
+
"min",
|
|
9143
|
+
"max",
|
|
9144
|
+
"sum",
|
|
9145
|
+
"any",
|
|
9146
|
+
"all"
|
|
9147
|
+
]),
|
|
9148
|
+
reads: z.array(CompositionFieldReadSchema).min(1).max(64),
|
|
9149
|
+
/** `value` is the reading; the others are the member counts. */
|
|
9150
|
+
publish: z.enum([
|
|
9151
|
+
"value",
|
|
9152
|
+
"included",
|
|
9153
|
+
"excluded",
|
|
9154
|
+
"total"
|
|
9155
|
+
]),
|
|
9156
|
+
/** What a `value` publish does when nobody is included. Counts ignore this. */
|
|
9157
|
+
empty: z.enum([
|
|
9158
|
+
"null",
|
|
9159
|
+
"omit",
|
|
9160
|
+
"offline"
|
|
9161
|
+
])
|
|
9162
|
+
});
|
|
9060
9163
|
var CompositionFieldSourceSchema = z.discriminatedUnion("kind", [
|
|
9061
9164
|
CompositionFromSourceSchema,
|
|
9062
9165
|
CompositionExpressionSourceSchema,
|
|
9063
|
-
CompositionCodeSourceSchema
|
|
9166
|
+
CompositionCodeSourceSchema,
|
|
9167
|
+
CompositionAggregateSourceSchema
|
|
9064
9168
|
]);
|
|
9065
9169
|
/** `forward`: RESERVED for slice 3. Argument mapping arrives there as an additive optional `args`. */
|
|
9066
9170
|
var CompositionForwardCommandSchema = z.object({
|
|
@@ -9125,7 +9229,13 @@ var CompositionExistingTargetSchema = z.object({
|
|
|
9125
9229
|
var CompositionTargetSchema = z.discriminatedUnion("kind", [CompositionNewTargetSchema, CompositionExistingTargetSchema]);
|
|
9126
9230
|
var CompositionSchema = z.object({
|
|
9127
9231
|
target: CompositionTargetSchema,
|
|
9128
|
-
features: z.array(CompositionFeatureSchema).min(1).max(16)
|
|
9232
|
+
features: z.array(CompositionFeatureSchema).min(1).max(16),
|
|
9233
|
+
/**
|
|
9234
|
+
* Present when the block is a helper template. Absent on a free
|
|
9235
|
+
* composition (the room example, device 4603). The editor reads this,
|
|
9236
|
+
* never the compiled features.
|
|
9237
|
+
*/
|
|
9238
|
+
helper: HelperSpecSchema.optional()
|
|
9129
9239
|
}).superRefine((composition, ctx) => {
|
|
9130
9240
|
const seen = /* @__PURE__ */ new Set();
|
|
9131
9241
|
composition.features.forEach((feature, index) => {
|
|
@@ -9172,6 +9282,7 @@ function fieldReads(source) {
|
|
|
9172
9282
|
cap: binding.cap,
|
|
9173
9283
|
fieldPath: binding.fieldPath
|
|
9174
9284
|
}] : []);
|
|
9285
|
+
case "aggregate": return source.reads;
|
|
9175
9286
|
case "code": return [];
|
|
9176
9287
|
}
|
|
9177
9288
|
}
|
|
@@ -17363,7 +17474,10 @@ var CompositionProblemCodeSchema = z.enum([
|
|
|
17363
17474
|
"target-missing",
|
|
17364
17475
|
"target-unreadable",
|
|
17365
17476
|
"not-replaceable",
|
|
17366
|
-
"native-capability-replace"
|
|
17477
|
+
"native-capability-replace",
|
|
17478
|
+
"mixed-quantity",
|
|
17479
|
+
"helper-mismatch",
|
|
17480
|
+
"member-constraint"
|
|
17367
17481
|
]);
|
|
17368
17482
|
/** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
|
|
17369
17483
|
var CompositionProblemSchema = z.object({
|
|
@@ -17387,7 +17501,8 @@ var CompositionUnavailableModeSchema = z.enum([
|
|
|
17387
17501
|
"null",
|
|
17388
17502
|
"offline",
|
|
17389
17503
|
"unknown",
|
|
17390
|
-
"release"
|
|
17504
|
+
"release",
|
|
17505
|
+
"omit"
|
|
17391
17506
|
]);
|
|
17392
17507
|
/**
|
|
17393
17508
|
* `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
|
|
@@ -17749,6 +17864,24 @@ function inferExpressionKinds(input) {
|
|
|
17749
17864
|
* planner (`composition-items.ts`). Internal to the composition module — NOT
|
|
17750
17865
|
* in the barrel: these are planner internals, not a published contract.
|
|
17751
17866
|
*/
|
|
17867
|
+
function aggregateKinds(source) {
|
|
17868
|
+
if (source.publish !== "value") return {
|
|
17869
|
+
ok: true,
|
|
17870
|
+
kinds: new Set(["number"])
|
|
17871
|
+
};
|
|
17872
|
+
if (source.reducer === "any" || source.reducer === "all") return {
|
|
17873
|
+
ok: true,
|
|
17874
|
+
kinds: new Set(["boolean"])
|
|
17875
|
+
};
|
|
17876
|
+
if (source.empty === "null") return {
|
|
17877
|
+
ok: true,
|
|
17878
|
+
kinds: new Set(["number", "null"])
|
|
17879
|
+
};
|
|
17880
|
+
return {
|
|
17881
|
+
ok: true,
|
|
17882
|
+
kinds: new Set(["number"])
|
|
17883
|
+
};
|
|
17884
|
+
}
|
|
17752
17885
|
function readKinds(read, lookupCap) {
|
|
17753
17886
|
const def = lookupCap(read.cap);
|
|
17754
17887
|
const schema = def?.runtimeState ?? def?.status?.schema;
|
|
@@ -17776,6 +17909,7 @@ function sourceKinds(source, lookupCap) {
|
|
|
17776
17909
|
code: "reserved-variant",
|
|
17777
17910
|
message: "code fields arrive in slice 4"
|
|
17778
17911
|
};
|
|
17912
|
+
if (source.kind === "aggregate") return aggregateKinds(source);
|
|
17779
17913
|
const bindingKinds = /* @__PURE__ */ new Map();
|
|
17780
17914
|
for (const [name, binding] of Object.entries(source.bindings)) {
|
|
17781
17915
|
if (binding.kind === "literal") {
|
|
@@ -17970,6 +18104,379 @@ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
|
|
|
17970
18104
|
};
|
|
17971
18105
|
}
|
|
17972
18106
|
/**
|
|
18107
|
+
* One catalog row per base reading a group helper can build (D944 §3).
|
|
18108
|
+
* Data, not a form default written into the block.
|
|
18109
|
+
*/
|
|
18110
|
+
var NOW = {
|
|
18111
|
+
fieldPath: "lastFetchedAt",
|
|
18112
|
+
expr: "now"
|
|
18113
|
+
};
|
|
18114
|
+
var CHANGED = {
|
|
18115
|
+
fieldPath: "lastChangedAt",
|
|
18116
|
+
expr: "now"
|
|
18117
|
+
};
|
|
18118
|
+
function numeric(input) {
|
|
18119
|
+
return {
|
|
18120
|
+
...input,
|
|
18121
|
+
family: "numeric",
|
|
18122
|
+
deviceType: DeviceType.Sensor,
|
|
18123
|
+
memberDeviceType: DeviceType.Sensor,
|
|
18124
|
+
baseUnit: baseUnitOf(input.quantity),
|
|
18125
|
+
aggregates: NUMERIC_AGGREGATES,
|
|
18126
|
+
accept: [{
|
|
18127
|
+
cap: input.outputCap,
|
|
18128
|
+
fieldPath: input.outputField
|
|
18129
|
+
}]
|
|
18130
|
+
};
|
|
18131
|
+
}
|
|
18132
|
+
function binary(input) {
|
|
18133
|
+
return {
|
|
18134
|
+
...input,
|
|
18135
|
+
family: "binary",
|
|
18136
|
+
deviceType: DeviceType.Sensor,
|
|
18137
|
+
memberDeviceType: DeviceType.Sensor,
|
|
18138
|
+
quantity: "none",
|
|
18139
|
+
baseUnit: void 0,
|
|
18140
|
+
aggregates: BINARY_AGGREGATES,
|
|
18141
|
+
empty: "offline"
|
|
18142
|
+
};
|
|
18143
|
+
}
|
|
18144
|
+
var GROUP_TEMPLATES = [
|
|
18145
|
+
numeric({
|
|
18146
|
+
id: "temperature",
|
|
18147
|
+
summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
|
|
18148
|
+
role: DeviceRole.TemperatureSensor,
|
|
18149
|
+
outputCap: "temperature-sensor",
|
|
18150
|
+
outputField: "celsius",
|
|
18151
|
+
quantity: "temperature",
|
|
18152
|
+
defaultAggregate: "mean",
|
|
18153
|
+
empty: "null",
|
|
18154
|
+
companions: [NOW]
|
|
18155
|
+
}),
|
|
18156
|
+
numeric({
|
|
18157
|
+
id: "humidity",
|
|
18158
|
+
summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
18159
|
+
role: DeviceRole.HumiditySensor,
|
|
18160
|
+
outputCap: "humidity-sensor",
|
|
18161
|
+
outputField: "percent",
|
|
18162
|
+
quantity: "humidity",
|
|
18163
|
+
defaultAggregate: "mean",
|
|
18164
|
+
empty: "null",
|
|
18165
|
+
companions: [NOW]
|
|
18166
|
+
}),
|
|
18167
|
+
numeric({
|
|
18168
|
+
id: "pressure",
|
|
18169
|
+
summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
|
|
18170
|
+
role: DeviceRole.PressureSensor,
|
|
18171
|
+
outputCap: "pressure-sensor",
|
|
18172
|
+
outputField: "hpa",
|
|
18173
|
+
quantity: "pressure",
|
|
18174
|
+
defaultAggregate: "mean",
|
|
18175
|
+
empty: "null",
|
|
18176
|
+
companions: [NOW]
|
|
18177
|
+
}),
|
|
18178
|
+
numeric({
|
|
18179
|
+
id: "illuminance",
|
|
18180
|
+
summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
|
|
18181
|
+
role: DeviceRole.AmbientLightSensor,
|
|
18182
|
+
outputCap: "ambient-light-sensor",
|
|
18183
|
+
outputField: "lux",
|
|
18184
|
+
quantity: "illuminance",
|
|
18185
|
+
defaultAggregate: "mean",
|
|
18186
|
+
empty: "null",
|
|
18187
|
+
companions: [NOW]
|
|
18188
|
+
}),
|
|
18189
|
+
numeric({
|
|
18190
|
+
id: "power",
|
|
18191
|
+
summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
|
|
18192
|
+
role: DeviceRole.PowerSensor,
|
|
18193
|
+
outputCap: "power-meter",
|
|
18194
|
+
outputField: "watts",
|
|
18195
|
+
quantity: "power",
|
|
18196
|
+
defaultAggregate: "sum",
|
|
18197
|
+
empty: "omit",
|
|
18198
|
+
companions: [NOW]
|
|
18199
|
+
}),
|
|
18200
|
+
numeric({
|
|
18201
|
+
id: "energy",
|
|
18202
|
+
summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
|
|
18203
|
+
role: DeviceRole.EnergySensor,
|
|
18204
|
+
outputCap: "power-meter",
|
|
18205
|
+
outputField: "kwhTotal",
|
|
18206
|
+
quantity: "energy",
|
|
18207
|
+
defaultAggregate: "sum",
|
|
18208
|
+
empty: "omit",
|
|
18209
|
+
companions: [NOW]
|
|
18210
|
+
}),
|
|
18211
|
+
numeric({
|
|
18212
|
+
id: "battery",
|
|
18213
|
+
summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
18214
|
+
role: DeviceRole.BatterySensor,
|
|
18215
|
+
outputCap: "battery",
|
|
18216
|
+
outputField: "percentage",
|
|
18217
|
+
quantity: "percentage",
|
|
18218
|
+
defaultAggregate: "mean",
|
|
18219
|
+
empty: "null",
|
|
18220
|
+
companions: [
|
|
18221
|
+
{
|
|
18222
|
+
fieldPath: "charging",
|
|
18223
|
+
expr: "\"unknown\""
|
|
18224
|
+
},
|
|
18225
|
+
{
|
|
18226
|
+
fieldPath: "sleeping",
|
|
18227
|
+
expr: "false"
|
|
18228
|
+
},
|
|
18229
|
+
{
|
|
18230
|
+
fieldPath: "lastUpdated",
|
|
18231
|
+
expr: "now"
|
|
18232
|
+
}
|
|
18233
|
+
]
|
|
18234
|
+
}),
|
|
18235
|
+
numeric({
|
|
18236
|
+
id: "numeric",
|
|
18237
|
+
summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
|
|
18238
|
+
role: DeviceRole.NumericSensor,
|
|
18239
|
+
outputCap: "numeric-sensor",
|
|
18240
|
+
outputField: "value",
|
|
18241
|
+
quantity: "device",
|
|
18242
|
+
defaultAggregate: "mean",
|
|
18243
|
+
empty: "null",
|
|
18244
|
+
companions: [NOW]
|
|
18245
|
+
}),
|
|
18246
|
+
binary({
|
|
18247
|
+
id: "motion",
|
|
18248
|
+
summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
|
|
18249
|
+
role: DeviceRole.MotionSensor,
|
|
18250
|
+
outputCap: "motion",
|
|
18251
|
+
outputField: "detected",
|
|
18252
|
+
defaultAggregate: "any",
|
|
18253
|
+
accept: [{
|
|
18254
|
+
cap: "motion",
|
|
18255
|
+
fieldPath: "detected"
|
|
18256
|
+
}],
|
|
18257
|
+
companions: [{
|
|
18258
|
+
fieldPath: "lastDetectedAt",
|
|
18259
|
+
expr: "null"
|
|
18260
|
+
}, {
|
|
18261
|
+
fieldPath: "autoClearAfterMs",
|
|
18262
|
+
expr: "null"
|
|
18263
|
+
}]
|
|
18264
|
+
}),
|
|
18265
|
+
binary({
|
|
18266
|
+
id: "contact",
|
|
18267
|
+
summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
|
|
18268
|
+
role: DeviceRole.ContactSensor,
|
|
18269
|
+
outputCap: "contact",
|
|
18270
|
+
outputField: "entryOpen",
|
|
18271
|
+
defaultAggregate: "any",
|
|
18272
|
+
accept: [{
|
|
18273
|
+
cap: "contact",
|
|
18274
|
+
fieldPath: "entryOpen"
|
|
18275
|
+
}],
|
|
18276
|
+
companions: [CHANGED]
|
|
18277
|
+
}),
|
|
18278
|
+
binary({
|
|
18279
|
+
id: "leak",
|
|
18280
|
+
summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
|
|
18281
|
+
role: DeviceRole.LeakSensor,
|
|
18282
|
+
outputCap: "flood",
|
|
18283
|
+
outputField: "flooded",
|
|
18284
|
+
defaultAggregate: "any",
|
|
18285
|
+
accept: [{
|
|
18286
|
+
cap: "flood",
|
|
18287
|
+
fieldPath: "flooded"
|
|
18288
|
+
}],
|
|
18289
|
+
companions: [CHANGED]
|
|
18290
|
+
}),
|
|
18291
|
+
binary({
|
|
18292
|
+
id: "smoke",
|
|
18293
|
+
summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
|
|
18294
|
+
role: DeviceRole.SmokeSensor,
|
|
18295
|
+
outputCap: "smoke",
|
|
18296
|
+
outputField: "detected",
|
|
18297
|
+
defaultAggregate: "any",
|
|
18298
|
+
accept: [{
|
|
18299
|
+
cap: "smoke",
|
|
18300
|
+
fieldPath: "detected"
|
|
18301
|
+
}],
|
|
18302
|
+
companions: [CHANGED]
|
|
18303
|
+
}),
|
|
18304
|
+
binary({
|
|
18305
|
+
id: "occupancy",
|
|
18306
|
+
summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
|
|
18307
|
+
role: DeviceRole.BinarySensor,
|
|
18308
|
+
outputCap: "binary",
|
|
18309
|
+
outputField: "on",
|
|
18310
|
+
defaultAggregate: "any",
|
|
18311
|
+
accept: [{
|
|
18312
|
+
cap: "motion",
|
|
18313
|
+
fieldPath: "detected"
|
|
18314
|
+
}, {
|
|
18315
|
+
cap: "binary",
|
|
18316
|
+
fieldPath: "on"
|
|
18317
|
+
}],
|
|
18318
|
+
companions: [CHANGED]
|
|
18319
|
+
}),
|
|
18320
|
+
binary({
|
|
18321
|
+
id: "binary",
|
|
18322
|
+
summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
|
|
18323
|
+
role: DeviceRole.BinarySensor,
|
|
18324
|
+
outputCap: "binary",
|
|
18325
|
+
outputField: "on",
|
|
18326
|
+
defaultAggregate: "any",
|
|
18327
|
+
accept: [{
|
|
18328
|
+
cap: "binary",
|
|
18329
|
+
fieldPath: "on"
|
|
18330
|
+
}],
|
|
18331
|
+
companions: [CHANGED]
|
|
18332
|
+
})
|
|
18333
|
+
];
|
|
18334
|
+
var BY_ID = new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
18335
|
+
function groupTemplate(id) {
|
|
18336
|
+
return BY_ID.get(id) ?? null;
|
|
18337
|
+
}
|
|
18338
|
+
function sameSource(a, b) {
|
|
18339
|
+
return a.addonId === b.addonId && a.stableId === b.stableId;
|
|
18340
|
+
}
|
|
18341
|
+
function sameMember(a, b) {
|
|
18342
|
+
return sameSource(a.source, b.source) && a.cap === b.cap && a.fieldPath === b.fieldPath;
|
|
18343
|
+
}
|
|
18344
|
+
function accepted(template, member) {
|
|
18345
|
+
return template.accept.some((field) => field.cap === member.cap && field.fieldPath === member.fieldPath);
|
|
18346
|
+
}
|
|
18347
|
+
function memberProblems(template, spec) {
|
|
18348
|
+
return spec.members.flatMap((member, index) => {
|
|
18349
|
+
const at = `helper.members[${index}]`;
|
|
18350
|
+
const problems = [];
|
|
18351
|
+
if (!accepted(template, member)) {
|
|
18352
|
+
const allowed = template.accept.map((field) => `${field.cap}.${field.fieldPath}`).join(" or ");
|
|
18353
|
+
problems.push(problem("member-constraint", at, `a ${template.id} group reads ${allowed}; this member is ${member.cap}.${member.fieldPath}`));
|
|
18354
|
+
}
|
|
18355
|
+
if (spec.members.slice(0, index).some((other) => sameMember(other, member))) problems.push(problem("member-constraint", at, "this member is already in the group"));
|
|
18356
|
+
return problems;
|
|
18357
|
+
});
|
|
18358
|
+
}
|
|
18359
|
+
function aggregateSource(template, spec, reads, publish) {
|
|
18360
|
+
return {
|
|
18361
|
+
kind: "aggregate",
|
|
18362
|
+
reducer: spec.aggregate,
|
|
18363
|
+
reads: [...reads],
|
|
18364
|
+
publish,
|
|
18365
|
+
empty: template.empty
|
|
18366
|
+
};
|
|
18367
|
+
}
|
|
18368
|
+
function expressionSource(expr) {
|
|
18369
|
+
return {
|
|
18370
|
+
kind: "expression",
|
|
18371
|
+
expr,
|
|
18372
|
+
bindings: {}
|
|
18373
|
+
};
|
|
18374
|
+
}
|
|
18375
|
+
function draftOf$1(template, spec) {
|
|
18376
|
+
const reads = spec.members.map((member) => ({
|
|
18377
|
+
source: member.source,
|
|
18378
|
+
cap: member.cap,
|
|
18379
|
+
fieldPath: member.fieldPath
|
|
18380
|
+
}));
|
|
18381
|
+
const companions = Object.fromEntries(template.companions.map((companion) => [companion.fieldPath, expressionSource(companion.expr)]));
|
|
18382
|
+
return {
|
|
18383
|
+
target: {
|
|
18384
|
+
kind: "new",
|
|
18385
|
+
type: template.deviceType,
|
|
18386
|
+
role: template.role
|
|
18387
|
+
},
|
|
18388
|
+
helper: spec,
|
|
18389
|
+
features: [{
|
|
18390
|
+
kind: "fields",
|
|
18391
|
+
cap: template.outputCap,
|
|
18392
|
+
fields: {
|
|
18393
|
+
[template.outputField]: aggregateSource(template, spec, reads, "value"),
|
|
18394
|
+
...companions
|
|
18395
|
+
}
|
|
18396
|
+
}, {
|
|
18397
|
+
kind: "fields",
|
|
18398
|
+
cap: "group-status",
|
|
18399
|
+
fields: {
|
|
18400
|
+
included: aggregateSource(template, spec, reads, "included"),
|
|
18401
|
+
excluded: aggregateSource(template, spec, reads, "excluded"),
|
|
18402
|
+
total: aggregateSource(template, spec, reads, "total")
|
|
18403
|
+
}
|
|
18404
|
+
}]
|
|
18405
|
+
};
|
|
18406
|
+
}
|
|
18407
|
+
function parseDraft(draft) {
|
|
18408
|
+
const parsed = CompositionSchema.safeParse(draft);
|
|
18409
|
+
if (parsed.success) return {
|
|
18410
|
+
ok: true,
|
|
18411
|
+
composition: parsed.data
|
|
18412
|
+
};
|
|
18413
|
+
return {
|
|
18414
|
+
ok: false,
|
|
18415
|
+
problems: [problem("helper-mismatch", "helper", `the template did not compile: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`)]
|
|
18416
|
+
};
|
|
18417
|
+
}
|
|
18418
|
+
/** Compile a group template to a new-device composition. Refusals are named. */
|
|
18419
|
+
function compileHelper(spec) {
|
|
18420
|
+
const template = groupTemplate(spec.templateId);
|
|
18421
|
+
if (template === null) return {
|
|
18422
|
+
ok: false,
|
|
18423
|
+
problems: [problem("member-constraint", "helper.templateId", `unknown template \`${spec.templateId}\``)]
|
|
18424
|
+
};
|
|
18425
|
+
if (!template.aggregates.includes(spec.aggregate)) {
|
|
18426
|
+
const allowed = template.aggregates.join(", ");
|
|
18427
|
+
return {
|
|
18428
|
+
ok: false,
|
|
18429
|
+
problems: [problem("kind-mismatch", "helper.aggregate", `a ${template.id} group aggregates by ${allowed}, not \`${spec.aggregate}\``)]
|
|
18430
|
+
};
|
|
18431
|
+
}
|
|
18432
|
+
const members = memberProblems(template, spec);
|
|
18433
|
+
if (members.length > 0) return {
|
|
18434
|
+
ok: false,
|
|
18435
|
+
problems: members
|
|
18436
|
+
};
|
|
18437
|
+
return parseDraft(draftOf$1(template, spec));
|
|
18438
|
+
}
|
|
18439
|
+
/** The built body, ignoring `helper`, so a save can demand one writer. */
|
|
18440
|
+
function sameHelperBody(stored, compiled) {
|
|
18441
|
+
return JSON.stringify(stored.target) === JSON.stringify(compiled.target) && JSON.stringify(stored.features) === JSON.stringify(compiled.features);
|
|
18442
|
+
}
|
|
18443
|
+
function quantityProblems(spec, facts) {
|
|
18444
|
+
const template = groupTemplate(spec.templateId);
|
|
18445
|
+
if (template === null || template.family !== "numeric") return [];
|
|
18446
|
+
const known = spec.members.flatMap((member, index) => {
|
|
18447
|
+
const quantity = facts.get(compositionSourceKey(member.source))?.quantity;
|
|
18448
|
+
return quantity === void 0 ? [] : [{
|
|
18449
|
+
index,
|
|
18450
|
+
quantity
|
|
18451
|
+
}];
|
|
18452
|
+
});
|
|
18453
|
+
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}`)]);
|
|
18454
|
+
const distinct = [...new Set(known.map((member) => member.quantity))];
|
|
18455
|
+
if (distinct.length <= 1) return [];
|
|
18456
|
+
return [problem("mixed-quantity", "helper.members", `a numeric group requires one quantity; members report ${distinct.join(" and ")}`)];
|
|
18457
|
+
}
|
|
18458
|
+
function deviceTypeProblems(spec, facts) {
|
|
18459
|
+
const template = groupTemplate(spec.templateId);
|
|
18460
|
+
if (template === null) return [];
|
|
18461
|
+
return spec.members.flatMap((member, index) => {
|
|
18462
|
+
const deviceType = facts.get(compositionSourceKey(member.source))?.deviceType;
|
|
18463
|
+
if (deviceType === void 0 || deviceType === template.memberDeviceType) return [];
|
|
18464
|
+
return [problem("member-constraint", `helper.members[${index}]`, `a ${template.id} group takes ${template.memberDeviceType} devices; this member is a ${deviceType}`)];
|
|
18465
|
+
});
|
|
18466
|
+
}
|
|
18467
|
+
/**
|
|
18468
|
+
* Problems that refuse a save. No `helper` key means a free composition: this
|
|
18469
|
+
* returns nothing, and expression semantics stay as they are.
|
|
18470
|
+
*/
|
|
18471
|
+
function helperSaveProblems(composition, facts) {
|
|
18472
|
+
const spec = composition.helper;
|
|
18473
|
+
if (spec === void 0) return [];
|
|
18474
|
+
const compiled = compileHelper(spec);
|
|
18475
|
+
if (!compiled.ok) return compiled.problems;
|
|
18476
|
+
if (!sameHelperBody(composition, compiled.composition)) return [problem("helper-mismatch", "helper", "the stored features are not this template — the helper spec is the only writer")];
|
|
18477
|
+
return [...quantityProblems(spec, facts), ...deviceTypeProblems(spec, facts)];
|
|
18478
|
+
}
|
|
18479
|
+
/**
|
|
17973
18480
|
* The field graph across every composition, and the cycles in it (D659).
|
|
17974
18481
|
*
|
|
17975
18482
|
* A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
|
|
@@ -18174,6 +18681,7 @@ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
|
|
|
18174
18681
|
const role = roleOf(source);
|
|
18175
18682
|
const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
|
|
18176
18683
|
if (!unavailable.ok) return refused(unavailable.problem);
|
|
18684
|
+
const omitEmpty = source.kind === "aggregate" && source.publish === "value" && source.empty === "omit";
|
|
18177
18685
|
const plan = {
|
|
18178
18686
|
cap: def.name,
|
|
18179
18687
|
path,
|
|
@@ -18181,7 +18689,7 @@ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
|
|
|
18181
18689
|
kind: tf.kind,
|
|
18182
18690
|
nullable: tf.nullable,
|
|
18183
18691
|
required: tf.required,
|
|
18184
|
-
unavailable: unavailable.unavailable,
|
|
18692
|
+
unavailable: omitEmpty ? "omit" : unavailable.unavailable,
|
|
18185
18693
|
role,
|
|
18186
18694
|
source,
|
|
18187
18695
|
reads: [...fieldReads(source)]
|
|
@@ -18446,10 +18954,15 @@ function validateComposition(composition, ctx) {
|
|
|
18446
18954
|
if (refusedFeatures.some((at) => item.path === at || item.path.startsWith(`${at}.`))) return false;
|
|
18447
18955
|
return refusedFeatures.length === 0 || item.path !== "features";
|
|
18448
18956
|
});
|
|
18957
|
+
const facts = new Map([...ctx.sources].flatMap(([key, source]) => source.kind === "found" ? [[key, {
|
|
18958
|
+
deviceType: source.deviceType,
|
|
18959
|
+
quantity: source.quantity
|
|
18960
|
+
}]] : []));
|
|
18449
18961
|
const problems = [
|
|
18450
18962
|
...plan.problems,
|
|
18451
18963
|
...sourceProblems(plan.fields, ctx, self),
|
|
18452
|
-
...cycles
|
|
18964
|
+
...cycles,
|
|
18965
|
+
...helperSaveProblems(composition, facts)
|
|
18453
18966
|
];
|
|
18454
18967
|
return {
|
|
18455
18968
|
ok: problems.length === 0,
|
|
@@ -65651,6 +66164,51 @@ var gasCapability = {
|
|
|
65651
66164
|
* whether persisting is worth a SQLite commit. */
|
|
65652
66165
|
volatileStateFields: ["lastChangedAt"]
|
|
65653
66166
|
};
|
|
66167
|
+
/**
|
|
66168
|
+
* How many members of a helper group answered (D944). Counts are integers,
|
|
66169
|
+
* not measurements: zero included is a real count, never a stand-in reading.
|
|
66170
|
+
* The reading itself stays on the output capability. History is none — the
|
|
66171
|
+
* series that later statistics read is the output cap's own history block.
|
|
66172
|
+
*/
|
|
66173
|
+
var GroupStatusSchema = z.object({
|
|
66174
|
+
/** Members whose current value entered the aggregate. */
|
|
66175
|
+
included: z.number().int().nonnegative().meta(readingMeta("count", {
|
|
66176
|
+
precision: 0,
|
|
66177
|
+
label: "Reporting"
|
|
66178
|
+
})),
|
|
66179
|
+
/** Members left out: offline, unknown, or not a finite reading (D393). */
|
|
66180
|
+
excluded: z.number().int().nonnegative().meta(readingMeta("count", {
|
|
66181
|
+
precision: 0,
|
|
66182
|
+
label: "Excluded"
|
|
66183
|
+
})),
|
|
66184
|
+
/** `included` + `excluded`. */
|
|
66185
|
+
total: z.number().int().nonnegative().meta(readingMeta("count", {
|
|
66186
|
+
precision: 0,
|
|
66187
|
+
label: "Members"
|
|
66188
|
+
}))
|
|
66189
|
+
});
|
|
66190
|
+
var groupStatusCapability = {
|
|
66191
|
+
name: "group-status",
|
|
66192
|
+
scope: "device",
|
|
66193
|
+
deviceNative: true,
|
|
66194
|
+
mode: "singleton",
|
|
66195
|
+
deviceTypes: [DeviceType.Sensor],
|
|
66196
|
+
methods: {},
|
|
66197
|
+
status: {
|
|
66198
|
+
schema: GroupStatusSchema,
|
|
66199
|
+
kind: "push"
|
|
66200
|
+
},
|
|
66201
|
+
runtimeState: GroupStatusSchema,
|
|
66202
|
+
/**
|
|
66203
|
+
* Runtime-state durability: **session** — recomputed from members on the
|
|
66204
|
+
* next source event; a restored count would outlive the readings it named.
|
|
66205
|
+
*
|
|
66206
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
66207
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
66208
|
+
*/
|
|
66209
|
+
durability: "session",
|
|
66210
|
+
history: "none"
|
|
66211
|
+
};
|
|
65654
66212
|
var HeatingValveStatusSchema = z.object({
|
|
65655
66213
|
/** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
|
|
65656
66214
|
position: z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
@@ -67715,12 +68273,18 @@ var motionCapability = {
|
|
|
67715
68273
|
methods: {
|
|
67716
68274
|
/**
|
|
67717
68275
|
* 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.
|
|
68276
|
+
* for consumers that don't need the full status object. A composed
|
|
68277
|
+
* group answers from its slice (D944): unknown throws, it is not off.
|
|
67722
68278
|
*/
|
|
67723
68279
|
isDetected: method(z.object({ deviceId: z.number() }), z.boolean()) },
|
|
68280
|
+
/**
|
|
68281
|
+
* A composed motion group serves `isDetected` from `detected` (D944).
|
|
68282
|
+
* Without this the cap stays uncomposable (`has-commands`).
|
|
68283
|
+
*/
|
|
68284
|
+
composedMethods: { isDetected: {
|
|
68285
|
+
kind: "slice",
|
|
68286
|
+
field: "detected"
|
|
68287
|
+
} },
|
|
67724
68288
|
events: {
|
|
67725
68289
|
/**
|
|
67726
68290
|
* Fires every time the runner transitions a camera between
|
|
@@ -76628,6 +77192,103 @@ function createDeviceExtensionProvider(options) {
|
|
|
76628
77192
|
}
|
|
76629
77193
|
};
|
|
76630
77194
|
}
|
|
77195
|
+
function isBinaryReducer(reducer) {
|
|
77196
|
+
return reducer === "any" || reducer === "all";
|
|
77197
|
+
}
|
|
77198
|
+
function includedValue(reducer, member) {
|
|
77199
|
+
if (member.state !== "fresh") return null;
|
|
77200
|
+
if (isBinaryReducer(reducer)) return typeof member.value === "boolean" ? member.value : null;
|
|
77201
|
+
return typeof member.value === "number" && Number.isFinite(member.value) ? member.value : null;
|
|
77202
|
+
}
|
|
77203
|
+
function median$1(values) {
|
|
77204
|
+
const sorted = [...values].toSorted((a, b) => a - b);
|
|
77205
|
+
const mid = Math.floor(sorted.length / 2);
|
|
77206
|
+
const right = sorted[mid];
|
|
77207
|
+
const left = sorted[mid - 1];
|
|
77208
|
+
if (right === void 0) throw new Error("median requires at least one finite number");
|
|
77209
|
+
if (sorted.length % 2 === 1) return right;
|
|
77210
|
+
if (left === void 0) throw new Error("median requires at least one finite number");
|
|
77211
|
+
return (left + right) / 2;
|
|
77212
|
+
}
|
|
77213
|
+
function reduceNumbers(reducer, values) {
|
|
77214
|
+
if (reducer === "min") return Math.min(...values);
|
|
77215
|
+
if (reducer === "max") return Math.max(...values);
|
|
77216
|
+
if (reducer === "sum") return values.reduce((sum, value) => sum + value, 0);
|
|
77217
|
+
if (reducer === "median") return median$1(values);
|
|
77218
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
77219
|
+
}
|
|
77220
|
+
function reduceBooleans(reducer, values) {
|
|
77221
|
+
return reducer === "all" ? values.every((value) => value) : values.some((value) => value);
|
|
77222
|
+
}
|
|
77223
|
+
function countsOf(included, total) {
|
|
77224
|
+
return {
|
|
77225
|
+
included,
|
|
77226
|
+
excluded: total - included,
|
|
77227
|
+
total
|
|
77228
|
+
};
|
|
77229
|
+
}
|
|
77230
|
+
function fatalReason(members) {
|
|
77231
|
+
const reasons = members.flatMap((member) => member.state === "unavailable" && member.fatal === true ? [member.reason ?? "source gone"] : []);
|
|
77232
|
+
return reasons.length === 0 ? null : reasons.join("; ");
|
|
77233
|
+
}
|
|
77234
|
+
function emptyOutcome(empty, counts) {
|
|
77235
|
+
if (empty === "null") return {
|
|
77236
|
+
kind: "value",
|
|
77237
|
+
value: null,
|
|
77238
|
+
counts
|
|
77239
|
+
};
|
|
77240
|
+
return {
|
|
77241
|
+
kind: "unavailable",
|
|
77242
|
+
fatal: false,
|
|
77243
|
+
reason: "no member included — unknown is not 0 or off",
|
|
77244
|
+
counts
|
|
77245
|
+
};
|
|
77246
|
+
}
|
|
77247
|
+
/** Pure. `members` is not mutated. An empty included set is null or unavailable, never 0 or false. */
|
|
77248
|
+
function evaluateAggregate(input) {
|
|
77249
|
+
const included = input.members.flatMap((member) => {
|
|
77250
|
+
const value = includedValue(input.reducer, member);
|
|
77251
|
+
return value === null ? [] : [value];
|
|
77252
|
+
});
|
|
77253
|
+
const counts = countsOf(included.length, input.members.length);
|
|
77254
|
+
const gone = fatalReason(input.members);
|
|
77255
|
+
if (gone !== null) return {
|
|
77256
|
+
kind: "unavailable",
|
|
77257
|
+
fatal: true,
|
|
77258
|
+
reason: gone,
|
|
77259
|
+
counts
|
|
77260
|
+
};
|
|
77261
|
+
if (input.publish === "included") return {
|
|
77262
|
+
kind: "value",
|
|
77263
|
+
value: counts.included,
|
|
77264
|
+
counts
|
|
77265
|
+
};
|
|
77266
|
+
if (input.publish === "excluded") return {
|
|
77267
|
+
kind: "value",
|
|
77268
|
+
value: counts.excluded,
|
|
77269
|
+
counts
|
|
77270
|
+
};
|
|
77271
|
+
if (input.publish === "total") return {
|
|
77272
|
+
kind: "value",
|
|
77273
|
+
value: counts.total,
|
|
77274
|
+
counts
|
|
77275
|
+
};
|
|
77276
|
+
if (included.length === 0) return emptyOutcome(input.empty, counts);
|
|
77277
|
+
if (isBinaryReducer(input.reducer)) {
|
|
77278
|
+
const flags = included.filter((value) => typeof value === "boolean");
|
|
77279
|
+
return {
|
|
77280
|
+
kind: "value",
|
|
77281
|
+
value: reduceBooleans(input.reducer, flags),
|
|
77282
|
+
counts
|
|
77283
|
+
};
|
|
77284
|
+
}
|
|
77285
|
+
const numbers = included.filter((value) => typeof value === "number");
|
|
77286
|
+
return {
|
|
77287
|
+
kind: "value",
|
|
77288
|
+
value: reduceNumbers(input.reducer, numbers),
|
|
77289
|
+
counts
|
|
77290
|
+
};
|
|
77291
|
+
}
|
|
76631
77292
|
/**
|
|
76632
77293
|
* Frozen unit → spec table. `•` in the doc marks each dimension's canonical
|
|
76633
77294
|
* unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
|
|
@@ -77133,11 +77794,45 @@ function expressionOutcome(source, read, now, stateful) {
|
|
|
77133
77794
|
deadline: deadlines.length > 0 ? Math.min(...deadlines) : null
|
|
77134
77795
|
};
|
|
77135
77796
|
}
|
|
77797
|
+
function aggregateRead(reading) {
|
|
77798
|
+
if (reading.state === "unavailable") return {
|
|
77799
|
+
state: "unavailable",
|
|
77800
|
+
fatal: reading.fatal,
|
|
77801
|
+
reason: reading.reason
|
|
77802
|
+
};
|
|
77803
|
+
return {
|
|
77804
|
+
state: reading.state,
|
|
77805
|
+
value: reading.value,
|
|
77806
|
+
...reading.state === "stale" ? { reason: reading.reason } : {}
|
|
77807
|
+
};
|
|
77808
|
+
}
|
|
77809
|
+
function aggregateField(source, read) {
|
|
77810
|
+
const outcome = evaluateAggregate({
|
|
77811
|
+
reducer: source.reducer,
|
|
77812
|
+
members: source.reads.map((item) => aggregateRead(read(item))),
|
|
77813
|
+
publish: source.publish,
|
|
77814
|
+
empty: source.empty
|
|
77815
|
+
});
|
|
77816
|
+
if (outcome.kind === "value") return {
|
|
77817
|
+
state: "fresh",
|
|
77818
|
+
value: outcome.value,
|
|
77819
|
+
reason: null,
|
|
77820
|
+
deadline: null
|
|
77821
|
+
};
|
|
77822
|
+
return {
|
|
77823
|
+
state: "unavailable",
|
|
77824
|
+
value: null,
|
|
77825
|
+
reason: outcome.reason,
|
|
77826
|
+
fatal: outcome.fatal,
|
|
77827
|
+
confirmed: true
|
|
77828
|
+
};
|
|
77829
|
+
}
|
|
77136
77830
|
function evaluateCompositionField(input) {
|
|
77137
77831
|
const { source } = input;
|
|
77138
77832
|
switch (source.kind) {
|
|
77139
77833
|
case "from": return fromReading(input.read(source), source);
|
|
77140
77834
|
case "expression": return expressionOutcome(source, input.read, input.now, input.stateful);
|
|
77835
|
+
case "aggregate": return aggregateField(source, input.read);
|
|
77141
77836
|
case "code": return {
|
|
77142
77837
|
state: "unavailable",
|
|
77143
77838
|
value: null,
|
|
@@ -77484,6 +78179,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
77484
78179
|
featureProbe: featureProbeCapability,
|
|
77485
78180
|
flood: floodCapability,
|
|
77486
78181
|
gas: gasCapability,
|
|
78182
|
+
groupStatus: groupStatusCapability,
|
|
77487
78183
|
heatingValve: heatingValveCapability,
|
|
77488
78184
|
humidifier: humidifierCapability,
|
|
77489
78185
|
humiditySensor: humiditySensorCapability,
|
|
@@ -81746,6 +82442,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
81746
82442
|
access: "view",
|
|
81747
82443
|
role: "viewer"
|
|
81748
82444
|
},
|
|
82445
|
+
"groupStatus.getStatus": {
|
|
82446
|
+
capName: "group-status",
|
|
82447
|
+
capScope: "device",
|
|
82448
|
+
addonId: null,
|
|
82449
|
+
access: "view",
|
|
82450
|
+
role: "viewer"
|
|
82451
|
+
},
|
|
81749
82452
|
"heatingValve.getStatus": {
|
|
81750
82453
|
capName: "heating-valve",
|
|
81751
82454
|
capScope: "device",
|
|
@@ -91568,6 +92271,33 @@ var SENSOR_CAP_VOCABULARY = {
|
|
|
91568
92271
|
icon: "flame"
|
|
91569
92272
|
}]
|
|
91570
92273
|
},
|
|
92274
|
+
"group-status": {
|
|
92275
|
+
scope: "device",
|
|
92276
|
+
noun: t("Group", "Gruppo"),
|
|
92277
|
+
state: [
|
|
92278
|
+
{
|
|
92279
|
+
kind: "numeric",
|
|
92280
|
+
field: "included",
|
|
92281
|
+
quantity: t("the members reporting", "i membri che rispondono"),
|
|
92282
|
+
label: t("Reporting", "Rispondono"),
|
|
92283
|
+
icon: "users"
|
|
92284
|
+
},
|
|
92285
|
+
{
|
|
92286
|
+
kind: "numeric",
|
|
92287
|
+
field: "excluded",
|
|
92288
|
+
quantity: t("the members left out", "i membri esclusi"),
|
|
92289
|
+
label: t("Excluded", "Esclusi"),
|
|
92290
|
+
icon: "users"
|
|
92291
|
+
},
|
|
92292
|
+
{
|
|
92293
|
+
kind: "numeric",
|
|
92294
|
+
field: "total",
|
|
92295
|
+
quantity: t("the member count", "il numero di membri"),
|
|
92296
|
+
label: t("Members", "Membri"),
|
|
92297
|
+
icon: "sigma"
|
|
92298
|
+
}
|
|
92299
|
+
]
|
|
92300
|
+
},
|
|
91571
92301
|
"humidity-sensor": {
|
|
91572
92302
|
scope: "device",
|
|
91573
92303
|
noun: t("Humidity", "Umidità"),
|
|
@@ -92104,6 +92834,7 @@ var CAP_NAMES_WITH_STATUS = [
|
|
|
92104
92834
|
"feature-probe",
|
|
92105
92835
|
"flood",
|
|
92106
92836
|
"gas",
|
|
92837
|
+
"group-status",
|
|
92107
92838
|
"heating-valve",
|
|
92108
92839
|
"humidifier",
|
|
92109
92840
|
"humidity-sensor",
|
|
@@ -92233,6 +92964,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
|
|
|
92233
92964
|
filesystemBrowseCapability,
|
|
92234
92965
|
floodCapability,
|
|
92235
92966
|
gasCapability,
|
|
92967
|
+
groupStatusCapability,
|
|
92236
92968
|
heatingValveCapability,
|
|
92237
92969
|
humidifierCapability,
|
|
92238
92970
|
humiditySensorCapability,
|
|
@@ -93307,6 +94039,11 @@ Object.freeze({
|
|
|
93307
94039
|
form: "single",
|
|
93308
94040
|
optional: false
|
|
93309
94041
|
}],
|
|
94042
|
+
"groupStatus.getStatus": [{
|
|
94043
|
+
name: "deviceId",
|
|
94044
|
+
form: "single",
|
|
94045
|
+
optional: false
|
|
94046
|
+
}],
|
|
93310
94047
|
"heatingValve.getStatus": [{
|
|
93311
94048
|
name: "deviceId",
|
|
93312
94049
|
form: "single",
|
|
@@ -95116,7 +95853,7 @@ Object.freeze({
|
|
|
95116
95853
|
});
|
|
95117
95854
|
/**
|
|
95118
95855
|
* Per-cap runtime-state policy, projected from every `*.cap.ts` that declares
|
|
95119
|
-
* `runtimeState`.
|
|
95856
|
+
* `runtimeState`. 73 caps: 38 `restored`, 35 `session`.
|
|
95120
95857
|
*
|
|
95121
95858
|
* A cap absent from this map has no runtime-state slice at all. A cap PRESENT
|
|
95122
95859
|
* with `durability: 'session'` has one and has decided not to keep it.
|
|
@@ -95234,6 +95971,10 @@ var RUNTIME_STATE_POLICY = {
|
|
|
95234
95971
|
durability: "restored",
|
|
95235
95972
|
volatileFields: ["lastChangedAt"]
|
|
95236
95973
|
},
|
|
95974
|
+
"group-status": {
|
|
95975
|
+
durability: "session",
|
|
95976
|
+
volatileFields: []
|
|
95977
|
+
},
|
|
95237
95978
|
"heating-valve": {
|
|
95238
95979
|
durability: "session",
|
|
95239
95980
|
volatileFields: ["lastFetchedAt"]
|
|
@@ -95428,7 +96169,7 @@ var SESSION_ONLY_POLICY = {
|
|
|
95428
96169
|
};
|
|
95429
96170
|
/**
|
|
95430
96171
|
* Cap history catalog, projected from every `*.cap.ts` `history` block.
|
|
95431
|
-
* 41 caps with series (75 series),
|
|
96172
|
+
* 41 caps with series (75 series), 31 caps answered `'none'`.
|
|
95432
96173
|
* Field keys are `runtimeState` paths (`a.b` into fixed objects only).
|
|
95433
96174
|
*/
|
|
95434
96175
|
var HISTORY_CATALOG = {
|