@camstack/addon-provider-rademacher 0.2.146 → 0.2.147

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/addon.js CHANGED
@@ -984,7 +984,7 @@ var Rademacher = class {
984
984
  }
985
985
  };
986
986
  //#endregion
987
- //#region ../types/dist/event-category-DHXxB8t9.mjs
987
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
988
988
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
989
989
  EventCategory["SystemBoot"] = "system.boot";
990
990
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1310,6 +1310,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1310
1310
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
1311
1311
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
1312
1312
  /**
1313
+ * A camera's package LLM confirm has failed closed three times in a row
1314
+ * (D931, 2026-10-26). One edge per camera until
1315
+ * {@link PipelinePackageLlmAnswered}.
1316
+ */
1317
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
1318
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
1319
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
1320
+ /**
1313
1321
  * Per-camera pipeline config was mutated by the orchestrator
1314
1322
  * (a settings change via `setAgentInferenceDevices` /
1315
1323
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -6419,7 +6427,7 @@ var ZodIssueCode = {
6419
6427
  var ZodFirstPartyTypeKind;
6420
6428
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6421
6429
  //#endregion
6422
- //#region ../types/dist/sleep-BErAxhul.mjs
6430
+ //#region ../types/dist/sleep-bykKrTgu.mjs
6423
6431
  /**
6424
6432
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6425
6433
  * window to float samples (D455).
@@ -8973,6 +8981,99 @@ var RecordKindDeclarationSchema = object({
8973
8981
  }).strict();
8974
8982
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8975
8983
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8984
+ var READING_QUANTITIES = [
8985
+ "temperature",
8986
+ "pressure",
8987
+ "speed",
8988
+ "precipitation",
8989
+ "precipitation-rate",
8990
+ "length",
8991
+ "area",
8992
+ "illuminance",
8993
+ "irradiance",
8994
+ "power",
8995
+ "energy",
8996
+ "voltage",
8997
+ "current",
8998
+ "apparent-power",
8999
+ "humidity",
9000
+ "percentage",
9001
+ "concentration-volume",
9002
+ "concentration-mass",
9003
+ "concentration-ppb",
9004
+ "volume",
9005
+ "duration",
9006
+ "duration-ms",
9007
+ "information",
9008
+ "sound-level",
9009
+ "signal-level",
9010
+ "colour-temperature",
9011
+ "angle",
9012
+ "bitrate",
9013
+ "rate",
9014
+ "mass",
9015
+ "index",
9016
+ "ratio",
9017
+ "count",
9018
+ "none",
9019
+ "device"
9020
+ ];
9021
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
9022
+ new Set(READING_QUANTITIES);
9023
+ /**
9024
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
9025
+ * (or an index / ratio / count). `device` means the cap has no single unit.
9026
+ */
9027
+ var QUANTITY_BASE = {
9028
+ temperature: "°C",
9029
+ pressure: "hPa",
9030
+ speed: "m/s",
9031
+ precipitation: "mm",
9032
+ "precipitation-rate": "mm/h",
9033
+ length: "m",
9034
+ area: "m²",
9035
+ illuminance: "lx",
9036
+ irradiance: "W/m²",
9037
+ power: "W",
9038
+ energy: "kWh",
9039
+ voltage: "V",
9040
+ current: "A",
9041
+ "apparent-power": "VA",
9042
+ humidity: "%",
9043
+ percentage: "%",
9044
+ "concentration-volume": "ppm",
9045
+ "concentration-mass": "µg/m³",
9046
+ "concentration-ppb": "ppb",
9047
+ volume: "L",
9048
+ duration: "s",
9049
+ "duration-ms": "ms",
9050
+ information: "B",
9051
+ "sound-level": "dBFS",
9052
+ "signal-level": "dBm",
9053
+ "colour-temperature": "mired",
9054
+ angle: "°",
9055
+ bitrate: "kbps",
9056
+ rate: "fps",
9057
+ mass: "kg",
9058
+ index: "none",
9059
+ ratio: "none",
9060
+ count: "none",
9061
+ none: "none",
9062
+ device: "device"
9063
+ };
9064
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
9065
+ function baseUnitOf(quantity) {
9066
+ const symbol = QUANTITY_BASE[quantity];
9067
+ if (symbol === "none" || symbol === "device") return void 0;
9068
+ return symbol;
9069
+ }
9070
+ function readingMeta(quantity, extras) {
9071
+ return {
9072
+ quantity,
9073
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
9074
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
9075
+ };
9076
+ }
8976
9077
  /** Shared Zod schemas used across streaming capabilities. */
8977
9078
  var CamProfileSchema = _enum([
8978
9079
  "high",
@@ -9000,8 +9101,8 @@ var CamStreamKindSchema = _enum([
9000
9101
  "derived"
9001
9102
  ]);
9002
9103
  var CamStreamResolutionSchema = object({
9003
- width: number().int().positive(),
9004
- height: number().int().positive()
9104
+ width: number().int().positive().meta(readingMeta("none")),
9105
+ height: number().int().positive().meta(readingMeta("none"))
9005
9106
  });
9006
9107
  var CameraStreamSchema = object({
9007
9108
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -9519,6 +9620,8 @@ var CAP_NAMES = Object.freeze([
9519
9620
  "filesystem-browse",
9520
9621
  "flood",
9521
9622
  "gas",
9623
+ "group-commands",
9624
+ "group-status",
9522
9625
  "heating-valve",
9523
9626
  "humidifier",
9524
9627
  "humidity-sensor",
@@ -9648,14 +9751,6 @@ Object.freeze({
9648
9751
  {
9649
9752
  "path": "aqi",
9650
9753
  "valueType": "number"
9651
- },
9652
- {
9653
- "path": "unit",
9654
- "valueType": "string"
9655
- },
9656
- {
9657
- "path": "precision",
9658
- "valueType": "number"
9659
9754
  }
9660
9755
  ],
9661
9756
  "alarm-panel": [
@@ -9691,20 +9786,10 @@ Object.freeze({
9691
9786
  "valueType": "boolean"
9692
9787
  }
9693
9788
  ],
9694
- "ambient-light-sensor": [
9695
- {
9696
- "path": "lux",
9697
- "valueType": "number"
9698
- },
9699
- {
9700
- "path": "unit",
9701
- "valueType": "string"
9702
- },
9703
- {
9704
- "path": "precision",
9705
- "valueType": "number"
9706
- }
9707
- ],
9789
+ "ambient-light-sensor": [{
9790
+ "path": "lux",
9791
+ "valueType": "number"
9792
+ }],
9708
9793
  "audio-metrics": [
9709
9794
  {
9710
9795
  "path": "ts",
@@ -9784,6 +9869,10 @@ Object.freeze({
9784
9869
  "path": "lastUpdated",
9785
9870
  "valueType": "number"
9786
9871
  },
9872
+ {
9873
+ "path": "volts",
9874
+ "valueType": "number"
9875
+ },
9787
9876
  {
9788
9877
  "path": "lastContactAt",
9789
9878
  "valueType": "number"
@@ -10026,10 +10115,6 @@ Object.freeze({
10026
10115
  "datetime"
10027
10116
  ]
10028
10117
  },
10029
- {
10030
- "path": "unit",
10031
- "valueType": "string"
10032
- },
10033
10118
  {
10034
10119
  "path": "min",
10035
10120
  "valueType": "number"
@@ -10041,19 +10126,6 @@ Object.freeze({
10041
10126
  {
10042
10127
  "path": "step",
10043
10128
  "valueType": "number"
10044
- },
10045
- {
10046
- "path": "precision",
10047
- "valueType": "number"
10048
- },
10049
- {
10050
- "path": "format",
10051
- "valueType": "enum",
10052
- "enumValues": [
10053
- "date",
10054
- "time",
10055
- "datetime"
10056
- ]
10057
10129
  }
10058
10130
  ],
10059
10131
  "cover": [
@@ -10294,13 +10366,8 @@ Object.freeze({
10294
10366
  "path": "value",
10295
10367
  "valueType": "string"
10296
10368
  }, {
10297
- "path": "format",
10298
- "valueType": "enum",
10299
- "enumValues": [
10300
- "date",
10301
- "time",
10302
- "datetime"
10303
- ]
10369
+ "path": "timestamp",
10370
+ "valueType": "boolean"
10304
10371
  }],
10305
10372
  "event-emitter": [
10306
10373
  {
@@ -10373,6 +10440,20 @@ Object.freeze({
10373
10440
  "path": "detected",
10374
10441
  "valueType": "boolean"
10375
10442
  }],
10443
+ "group-status": [
10444
+ {
10445
+ "path": "included",
10446
+ "valueType": "number"
10447
+ },
10448
+ {
10449
+ "path": "excluded",
10450
+ "valueType": "number"
10451
+ },
10452
+ {
10453
+ "path": "total",
10454
+ "valueType": "number"
10455
+ }
10456
+ ],
10376
10457
  "heating-valve": [{
10377
10458
  "path": "position",
10378
10459
  "valueType": "number"
@@ -10407,20 +10488,10 @@ Object.freeze({
10407
10488
  "valueType": "number"
10408
10489
  }
10409
10490
  ],
10410
- "humidity-sensor": [
10411
- {
10412
- "path": "percent",
10413
- "valueType": "number"
10414
- },
10415
- {
10416
- "path": "unit",
10417
- "valueType": "string"
10418
- },
10419
- {
10420
- "path": "precision",
10421
- "valueType": "number"
10422
- }
10423
- ],
10491
+ "humidity-sensor": [{
10492
+ "path": "percent",
10493
+ "valueType": "number"
10494
+ }],
10424
10495
  "image": [{
10425
10496
  "path": "url",
10426
10497
  "valueType": "string"
@@ -10759,20 +10830,10 @@ Object.freeze({
10759
10830
  "valueType": "boolean"
10760
10831
  }
10761
10832
  ],
10762
- "numeric-sensor": [
10763
- {
10764
- "path": "value",
10765
- "valueType": "number"
10766
- },
10767
- {
10768
- "path": "unit",
10769
- "valueType": "string"
10770
- },
10771
- {
10772
- "path": "precision",
10773
- "valueType": "number"
10774
- }
10775
- ],
10833
+ "numeric-sensor": [{
10834
+ "path": "value",
10835
+ "valueType": "number"
10836
+ }],
10776
10837
  "pet-feeder": [
10777
10838
  {
10778
10839
  "path": "foodLevel",
@@ -10864,14 +10925,6 @@ Object.freeze({
10864
10925
  {
10865
10926
  "path": "powerFactor",
10866
10927
  "valueType": "number"
10867
- },
10868
- {
10869
- "path": "unit",
10870
- "valueType": "string"
10871
- },
10872
- {
10873
- "path": "precision",
10874
- "valueType": "number"
10875
10928
  }
10876
10929
  ],
10877
10930
  "presence": [
@@ -10900,20 +10953,10 @@ Object.freeze({
10900
10953
  "valueType": "number"
10901
10954
  }
10902
10955
  ],
10903
- "pressure-sensor": [
10904
- {
10905
- "path": "hpa",
10906
- "valueType": "number"
10907
- },
10908
- {
10909
- "path": "unit",
10910
- "valueType": "string"
10911
- },
10912
- {
10913
- "path": "precision",
10914
- "valueType": "number"
10915
- }
10916
- ],
10956
+ "pressure-sensor": [{
10957
+ "path": "hpa",
10958
+ "valueType": "number"
10959
+ }],
10917
10960
  "privacy-mask": [{
10918
10961
  "path": "enabled",
10919
10962
  "valueType": "boolean"
@@ -11224,20 +11267,10 @@ Object.freeze({
11224
11267
  "path": "tampered",
11225
11268
  "valueType": "boolean"
11226
11269
  }],
11227
- "temperature-sensor": [
11228
- {
11229
- "path": "celsius",
11230
- "valueType": "number"
11231
- },
11232
- {
11233
- "path": "unit",
11234
- "valueType": "string"
11235
- },
11236
- {
11237
- "path": "precision",
11238
- "valueType": "number"
11239
- }
11240
- ],
11270
+ "temperature-sensor": [{
11271
+ "path": "celsius",
11272
+ "valueType": "number"
11273
+ }],
11241
11274
  "update": [
11242
11275
  {
11243
11276
  "path": "currentVersion",
@@ -11348,10 +11381,6 @@ Object.freeze({
11348
11381
  "path": "temperature",
11349
11382
  "valueType": "number"
11350
11383
  },
11351
- {
11352
- "path": "temperatureUnit",
11353
- "valueType": "string"
11354
- },
11355
11384
  {
11356
11385
  "path": "humidity",
11357
11386
  "valueType": "number"
@@ -11360,18 +11389,10 @@ Object.freeze({
11360
11389
  "path": "pressure",
11361
11390
  "valueType": "number"
11362
11391
  },
11363
- {
11364
- "path": "pressureUnit",
11365
- "valueType": "string"
11366
- },
11367
11392
  {
11368
11393
  "path": "windSpeed",
11369
11394
  "valueType": "number"
11370
11395
  },
11371
- {
11372
- "path": "windSpeedUnit",
11373
- "valueType": "string"
11374
- },
11375
11396
  {
11376
11397
  "path": "windBearing",
11377
11398
  "valueType": "number"
@@ -11774,7 +11795,7 @@ function errMsg(err) {
11774
11795
  return String(err);
11775
11796
  }
11776
11797
  //#endregion
11777
- //#region ../types/dist/composition-kEl9mevu.mjs
11798
+ //#region ../types/dist/composition-OhpuespA.mjs
11778
11799
  /**
11779
11800
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11780
11801
  *
@@ -12529,15 +12550,74 @@ function validateExpressionSource(src) {
12529
12550
  }
12530
12551
  return null;
12531
12552
  }
12532
- /** A composed device's stableId is this prefix plus the block id. */
12533
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12534
- /** Longest snippet a `code` source may carry (slice 4). */
12535
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12553
+ /**
12554
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12555
+ * id. Split out so helper schemas can name it without a cycle through
12556
+ * `composition.ts` (D659).
12557
+ */
12536
12558
  var CompositionSourceRefSchema = object({
12537
12559
  /** The owning addon; `stableId` is unique only within it. */
12538
12560
  addonId: string().min(1),
12539
12561
  stableId: string().min(1)
12540
12562
  });
12563
+ var GROUP_TEMPLATE_IDS = [
12564
+ "temperature",
12565
+ "humidity",
12566
+ "pressure",
12567
+ "illuminance",
12568
+ "power",
12569
+ "energy",
12570
+ "battery",
12571
+ "numeric",
12572
+ "motion",
12573
+ "contact",
12574
+ "leak",
12575
+ "smoke",
12576
+ "occupancy",
12577
+ "binary",
12578
+ "switch",
12579
+ "light"
12580
+ ];
12581
+ var NUMERIC_AGGREGATES = [
12582
+ "mean",
12583
+ "median",
12584
+ "min",
12585
+ "max",
12586
+ "sum"
12587
+ ];
12588
+ var BINARY_AGGREGATES = ["any", "all"];
12589
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12590
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12591
+ var AGGREGATE_REDUCERS = [
12592
+ "mean",
12593
+ "median",
12594
+ "min",
12595
+ "max",
12596
+ "sum",
12597
+ "any",
12598
+ "all"
12599
+ ];
12600
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12601
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12602
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12603
+ /** One member. The field is stored so occupancy can name motion or binary. */
12604
+ var HelperMemberSchema = object({
12605
+ source: CompositionSourceRefSchema,
12606
+ cap: string().min(1),
12607
+ fieldPath: string().min(1)
12608
+ });
12609
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
12610
+ kind: literal("group"),
12611
+ templateId: GroupTemplateIdSchema,
12612
+ aggregate: AggregateReducerSchema,
12613
+ members: array(HelperMemberSchema).min(1).max(64),
12614
+ /** Actuator groups only. Readings leave this unset. */
12615
+ active: ActiveCriterionSchema.optional()
12616
+ })]);
12617
+ /** A composed device's stableId is this prefix plus the block id. */
12618
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12619
+ /** Longest snippet a `code` source may carry (slice 4). */
12620
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12541
12621
  /** One field of one capability of one source device. */
12542
12622
  var CompositionFieldReadSchema = object({
12543
12623
  source: CompositionSourceRefSchema,
@@ -12581,7 +12661,33 @@ var CompositionCodeSourceSchema = object({
12581
12661
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12582
12662
  CompositionFromSourceSchema,
12583
12663
  CompositionExpressionSourceSchema,
12584
- CompositionCodeSourceSchema
12664
+ CompositionCodeSourceSchema,
12665
+ object({
12666
+ kind: literal("aggregate"),
12667
+ reducer: _enum([
12668
+ "mean",
12669
+ "median",
12670
+ "min",
12671
+ "max",
12672
+ "sum",
12673
+ "any",
12674
+ "all"
12675
+ ]),
12676
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12677
+ /** `value` is the reading; the others are the member counts. */
12678
+ publish: _enum([
12679
+ "value",
12680
+ "included",
12681
+ "excluded",
12682
+ "total"
12683
+ ]),
12684
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12685
+ empty: _enum([
12686
+ "null",
12687
+ "omit",
12688
+ "offline"
12689
+ ])
12690
+ })
12585
12691
  ]);
12586
12692
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12587
12693
  kind: literal("forward"),
@@ -12627,7 +12733,13 @@ var CompositionSchema = object({
12627
12733
  kind: literal("existing"),
12628
12734
  device: CompositionSourceRefSchema
12629
12735
  })]),
12630
- features: array(CompositionFeatureSchema).min(1).max(16)
12736
+ features: array(CompositionFeatureSchema).min(1).max(16),
12737
+ /**
12738
+ * Present when the block is a helper template. Absent on a free
12739
+ * composition (the room example, device 4603). The editor reads this,
12740
+ * never the compiled features.
12741
+ */
12742
+ helper: HelperSpecSchema.optional()
12631
12743
  }).superRefine((composition, ctx) => {
12632
12744
  const seen = /* @__PURE__ */ new Set();
12633
12745
  composition.features.forEach((feature, index) => {
@@ -18835,7 +18947,11 @@ var CompositionProblemSchema = object({
18835
18947
  "item-field-reserved",
18836
18948
  "target-missing",
18837
18949
  "target-unreadable",
18838
- "not-replaceable"
18950
+ "not-replaceable",
18951
+ "native-capability-replace",
18952
+ "mixed-quantity",
18953
+ "helper-mismatch",
18954
+ "member-constraint"
18839
18955
  ]),
18840
18956
  path: string(),
18841
18957
  message: string()
@@ -18856,7 +18972,8 @@ var CompositionUnavailableModeSchema = _enum([
18856
18972
  "null",
18857
18973
  "offline",
18858
18974
  "unknown",
18859
- "release"
18975
+ "release",
18976
+ "omit"
18860
18977
  ]);
18861
18978
  /**
18862
18979
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18893,9 +19010,11 @@ var CompositionFieldPlanSchema = object({
18893
19010
  reads: array(CompositionFieldReadSchema)
18894
19011
  });
18895
19012
  /**
18896
- * What a feature does to its target (D663): `new` builds a capability on a new
18897
- * device; on an existing device it `add`s a capability the device lacks, or
18898
- * `replace`s named fields of one it has natively.
19013
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
19014
+ * new device; on an existing device it `add`s a capability the device lacks.
19015
+ * Replacing a capability the device already serves is refused
19016
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
19017
+ * wire for a claim the hub already holds.
18899
19018
  */
18900
19019
  var CompositionFeatureModeSchema = _enum([
18901
19020
  "new",
@@ -19068,6 +19187,275 @@ Object.freeze({
19068
19187
  result: ["number", "null"]
19069
19188
  }
19070
19189
  });
19190
+ /**
19191
+ * One catalog row per base reading a group helper can build (D944 §3).
19192
+ * Data, not a form default written into the block.
19193
+ */
19194
+ var NOW = {
19195
+ fieldPath: "lastFetchedAt",
19196
+ expr: "now"
19197
+ };
19198
+ var CHANGED = {
19199
+ fieldPath: "lastChangedAt",
19200
+ expr: "now"
19201
+ };
19202
+ function numeric(input) {
19203
+ return {
19204
+ ...input,
19205
+ family: "numeric",
19206
+ deviceType: DeviceType.Sensor,
19207
+ memberDeviceType: DeviceType.Sensor,
19208
+ commandCaps: [],
19209
+ baseUnit: baseUnitOf(input.quantity),
19210
+ aggregates: NUMERIC_AGGREGATES,
19211
+ accept: [{
19212
+ cap: input.outputCap,
19213
+ fieldPath: input.outputField
19214
+ }]
19215
+ };
19216
+ }
19217
+ function binary(input) {
19218
+ return {
19219
+ ...input,
19220
+ family: "binary",
19221
+ deviceType: DeviceType.Sensor,
19222
+ memberDeviceType: DeviceType.Sensor,
19223
+ commandCaps: [],
19224
+ quantity: "none",
19225
+ baseUnit: void 0,
19226
+ aggregates: BINARY_AGGREGATES,
19227
+ empty: "offline"
19228
+ };
19229
+ }
19230
+ var GROUP_TEMPLATES = [
19231
+ numeric({
19232
+ id: "temperature",
19233
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19234
+ role: DeviceRole.TemperatureSensor,
19235
+ outputCap: "temperature-sensor",
19236
+ outputField: "celsius",
19237
+ quantity: "temperature",
19238
+ defaultAggregate: "mean",
19239
+ empty: "null",
19240
+ companions: [NOW]
19241
+ }),
19242
+ numeric({
19243
+ id: "humidity",
19244
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19245
+ role: DeviceRole.HumiditySensor,
19246
+ outputCap: "humidity-sensor",
19247
+ outputField: "percent",
19248
+ quantity: "humidity",
19249
+ defaultAggregate: "mean",
19250
+ empty: "null",
19251
+ companions: [NOW]
19252
+ }),
19253
+ numeric({
19254
+ id: "pressure",
19255
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19256
+ role: DeviceRole.PressureSensor,
19257
+ outputCap: "pressure-sensor",
19258
+ outputField: "hpa",
19259
+ quantity: "pressure",
19260
+ defaultAggregate: "mean",
19261
+ empty: "null",
19262
+ companions: [NOW]
19263
+ }),
19264
+ numeric({
19265
+ id: "illuminance",
19266
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19267
+ role: DeviceRole.AmbientLightSensor,
19268
+ outputCap: "ambient-light-sensor",
19269
+ outputField: "lux",
19270
+ quantity: "illuminance",
19271
+ defaultAggregate: "mean",
19272
+ empty: "null",
19273
+ companions: [NOW]
19274
+ }),
19275
+ numeric({
19276
+ id: "power",
19277
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19278
+ role: DeviceRole.PowerSensor,
19279
+ outputCap: "power-meter",
19280
+ outputField: "watts",
19281
+ quantity: "power",
19282
+ defaultAggregate: "sum",
19283
+ empty: "omit",
19284
+ companions: [NOW]
19285
+ }),
19286
+ numeric({
19287
+ id: "energy",
19288
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19289
+ role: DeviceRole.EnergySensor,
19290
+ outputCap: "power-meter",
19291
+ outputField: "kwhTotal",
19292
+ quantity: "energy",
19293
+ defaultAggregate: "sum",
19294
+ empty: "omit",
19295
+ companions: [NOW]
19296
+ }),
19297
+ numeric({
19298
+ id: "battery",
19299
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19300
+ role: DeviceRole.BatterySensor,
19301
+ outputCap: "battery",
19302
+ outputField: "percentage",
19303
+ quantity: "percentage",
19304
+ defaultAggregate: "mean",
19305
+ empty: "null",
19306
+ companions: [
19307
+ {
19308
+ fieldPath: "charging",
19309
+ expr: "\"unknown\""
19310
+ },
19311
+ {
19312
+ fieldPath: "sleeping",
19313
+ expr: "false"
19314
+ },
19315
+ {
19316
+ fieldPath: "lastUpdated",
19317
+ expr: "now"
19318
+ }
19319
+ ]
19320
+ }),
19321
+ numeric({
19322
+ id: "numeric",
19323
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19324
+ role: DeviceRole.NumericSensor,
19325
+ outputCap: "numeric-sensor",
19326
+ outputField: "value",
19327
+ quantity: "device",
19328
+ defaultAggregate: "mean",
19329
+ empty: "null",
19330
+ companions: [NOW]
19331
+ }),
19332
+ binary({
19333
+ id: "motion",
19334
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19335
+ role: DeviceRole.MotionSensor,
19336
+ outputCap: "motion",
19337
+ outputField: "detected",
19338
+ defaultAggregate: "any",
19339
+ accept: [{
19340
+ cap: "motion",
19341
+ fieldPath: "detected"
19342
+ }],
19343
+ companions: [{
19344
+ fieldPath: "lastDetectedAt",
19345
+ expr: "null"
19346
+ }, {
19347
+ fieldPath: "autoClearAfterMs",
19348
+ expr: "null"
19349
+ }]
19350
+ }),
19351
+ binary({
19352
+ id: "contact",
19353
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19354
+ role: DeviceRole.ContactSensor,
19355
+ outputCap: "contact",
19356
+ outputField: "entryOpen",
19357
+ defaultAggregate: "any",
19358
+ accept: [{
19359
+ cap: "contact",
19360
+ fieldPath: "entryOpen"
19361
+ }],
19362
+ companions: [CHANGED]
19363
+ }),
19364
+ binary({
19365
+ id: "leak",
19366
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19367
+ role: DeviceRole.LeakSensor,
19368
+ outputCap: "flood",
19369
+ outputField: "flooded",
19370
+ defaultAggregate: "any",
19371
+ accept: [{
19372
+ cap: "flood",
19373
+ fieldPath: "flooded"
19374
+ }],
19375
+ companions: [CHANGED]
19376
+ }),
19377
+ binary({
19378
+ id: "smoke",
19379
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19380
+ role: DeviceRole.SmokeSensor,
19381
+ outputCap: "smoke",
19382
+ outputField: "detected",
19383
+ defaultAggregate: "any",
19384
+ accept: [{
19385
+ cap: "smoke",
19386
+ fieldPath: "detected"
19387
+ }],
19388
+ companions: [CHANGED]
19389
+ }),
19390
+ binary({
19391
+ id: "occupancy",
19392
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19393
+ role: DeviceRole.BinarySensor,
19394
+ outputCap: "binary",
19395
+ outputField: "on",
19396
+ defaultAggregate: "any",
19397
+ accept: [{
19398
+ cap: "motion",
19399
+ fieldPath: "detected"
19400
+ }, {
19401
+ cap: "binary",
19402
+ fieldPath: "on"
19403
+ }],
19404
+ companions: [CHANGED]
19405
+ }),
19406
+ binary({
19407
+ id: "binary",
19408
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19409
+ role: DeviceRole.BinarySensor,
19410
+ outputCap: "binary",
19411
+ outputField: "on",
19412
+ defaultAggregate: "any",
19413
+ accept: [{
19414
+ cap: "binary",
19415
+ fieldPath: "on"
19416
+ }],
19417
+ companions: [CHANGED]
19418
+ }),
19419
+ actuator({
19420
+ id: "switch",
19421
+ 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.",
19422
+ deviceType: DeviceType.Switch,
19423
+ outputCap: "switch",
19424
+ commandCaps: ["switch"]
19425
+ }),
19426
+ actuator({
19427
+ id: "light",
19428
+ 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.",
19429
+ deviceType: DeviceType.Light,
19430
+ outputCap: "switch",
19431
+ commandCaps: [
19432
+ "switch",
19433
+ "brightness",
19434
+ "color"
19435
+ ]
19436
+ })
19437
+ ];
19438
+ function actuator(input) {
19439
+ return {
19440
+ ...input,
19441
+ family: "actuator",
19442
+ role: void 0,
19443
+ memberDeviceType: input.deviceType,
19444
+ outputField: "on",
19445
+ quantity: "none",
19446
+ baseUnit: void 0,
19447
+ aggregates: BINARY_AGGREGATES,
19448
+ defaultAggregate: "any",
19449
+ empty: "offline",
19450
+ accept: [{
19451
+ cap: "switch",
19452
+ fieldPath: "on"
19453
+ }],
19454
+ companions: [CHANGED]
19455
+ };
19456
+ }
19457
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19458
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19071
19459
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
19072
19460
  /**
19073
19461
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20894,12 +21282,12 @@ function buildDeviceAdminUrl(parts) {
20894
21282
  * providers detect renames by matching `uniqueId` and patch `id`
20895
21283
  * in-place so dispatch keeps working.
20896
21284
  *
20897
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20898
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20899
- * state push and are the single source of truth. The `device_class` tag is
20900
- * an HA-specific input that is consumed at the discovery boundary
20901
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20902
- * stored after that point.
21285
+ * **Display unit and precision are NOT stored here.** They are a read-time
21286
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21287
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21288
+ * neither is repeated on each sample. The `device_class` tag is an
21289
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21290
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20903
21291
  *
20904
21292
  * The `raw` escape hatch carries provider-specific attributes that don't
20905
21293
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20913,6 +21301,14 @@ var SourceInfoSchema = object({
20913
21301
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20914
21302
  * Used to detect rename when `id` changes. */
20915
21303
  uniqueId: string().optional(),
21304
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21305
+ quantity: ReadingQuantitySchema.optional(),
21306
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21307
+ dateTimeShape: _enum([
21308
+ "date",
21309
+ "time",
21310
+ "datetime"
21311
+ ]).optional(),
20916
21312
  /** Provider-specific extras that don't fit the structured slots.
20917
21313
  * Whitelist what goes here per provider — do NOT dump entire HA
20918
21314
  * attribute blobs (would explode the meta JSON column). */
@@ -42017,7 +42413,8 @@ method(object({
42017
42413
  "enum"
42018
42414
  ]),
42019
42415
  enumValues: array(string()).optional(),
42020
- item: boolean().optional()
42416
+ item: boolean().optional(),
42417
+ quantity: ReadingQuantitySchema.optional()
42021
42418
  })).readonly(),
42022
42419
  itemArray: object({
42023
42420
  path: string(),
@@ -45861,8 +46258,8 @@ var MaskPolygonShapeSchema = object({
45861
46258
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45862
46259
  var MaskGridShapeSchema = object({
45863
46260
  kind: literal("grid"),
45864
- gridWidth: number(),
45865
- gridHeight: number(),
46261
+ gridWidth: number().meta(readingMeta("none")),
46262
+ gridHeight: number().meta(readingMeta("none")),
45866
46263
  cells: array(boolean())
45867
46264
  });
45868
46265
  discriminatedUnion("kind", [
@@ -45974,6 +46371,8 @@ var NcSystemEventKindSchema = _enum([
45974
46371
  "detection-blind",
45975
46372
  "motion-noisy",
45976
46373
  "motion-calm",
46374
+ "package-llm-silent",
46375
+ "package-llm-answered",
45977
46376
  "recording-stalled",
45978
46377
  "recording-healthy",
45979
46378
  "recording-degraded",
@@ -57065,6 +57464,28 @@ var PackageVersionInfoSchema = object({
57065
57464
  deprecated: string().optional(),
57066
57465
  distTags: array(string()).readonly()
57067
57466
  });
57467
+ /**
57468
+ * One published document. `ok` is markdown from the registry. `missing` means
57469
+ * the registry answered and the file was not there. `error` is a registry or
57470
+ * tarball failure — never an empty string standing in for either.
57471
+ */
57472
+ var PackageDocBodySchema = discriminatedUnion("status", [
57473
+ object({
57474
+ status: literal("ok"),
57475
+ markdown: string()
57476
+ }),
57477
+ object({ status: literal("missing") }),
57478
+ object({
57479
+ status: literal("error"),
57480
+ message: string()
57481
+ })
57482
+ ]);
57483
+ var PackageDocsSchema = object({
57484
+ name: string(),
57485
+ version: string(),
57486
+ readme: PackageDocBodySchema,
57487
+ changelog: PackageDocBodySchema
57488
+ });
57068
57489
  var SearchResultSchema = object({
57069
57490
  name: string(),
57070
57491
  version: string(),
@@ -57283,7 +57704,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57283
57704
  }), object({ success: literal(true) }), {
57284
57705
  kind: "mutation",
57285
57706
  auth: "admin"
57286
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57707
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57708
+ name: string().min(1),
57709
+ version: string().min(1).optional()
57710
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57287
57711
  kind: "mutation",
57288
57712
  auth: "admin"
57289
57713
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57342,27 +57766,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57342
57766
  */
57343
57767
  var AirQualitySensorStatusSchema = object({
57344
57768
  /** Carbon dioxide concentration in ppm. */
57345
- co2Ppm: number().min(0).optional(),
57769
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57346
57770
  /** Total volatile organic compounds in ppb. */
57347
- vocPpb: number().min(0).optional(),
57771
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57348
57772
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57349
- pm25: number().min(0).optional(),
57773
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57350
57774
  /** Particulate matter ≤ 10 μm in µg/m³. */
57351
- pm10: number().min(0).optional(),
57775
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57352
57776
  /** Composite AQI value (typically 0..500). */
57353
- aqi: number().optional(),
57777
+ aqi: number().optional().meta(readingMeta("index")),
57354
57778
  /** Ms epoch when the slice was last updated. */
57355
- lastFetchedAt: number(),
57356
- /** Live display unit of the single metric this slice carries (e.g. HA
57357
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57358
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57359
- * per slice is unambiguous. */
57360
- unit: string().optional(),
57361
- /** Suggested decimal places for numeric display.
57362
- * Populated live from the upstream source when provided (e.g. HA
57363
- * `attributes.suggested_display_precision`). Falls back to
57364
- * auto-formatting when absent. */
57365
- precision: number().int().min(0).max(10).optional()
57779
+ lastFetchedAt: number()
57366
57780
  });
57367
57781
  var airQualitySensorCapability = {
57368
57782
  name: "air-quality-sensor",
@@ -57399,12 +57813,12 @@ var airQualitySensorCapability = {
57399
57813
  },
57400
57814
  pm25: {
57401
57815
  kind: "gauge",
57402
- unit: "ug/m3",
57816
+ unit: "µg/m³",
57403
57817
  deadband: 1
57404
57818
  },
57405
57819
  pm10: {
57406
57820
  kind: "gauge",
57407
- unit: "ug/m3",
57821
+ unit: "µg/m³",
57408
57822
  deadband: 1
57409
57823
  },
57410
57824
  aqi: {
@@ -57422,22 +57836,13 @@ var airQualitySensorCapability = {
57422
57836
  * entries with `device_class: illuminance`.
57423
57837
  */
57424
57838
  var AmbientLightSensorStatusSchema = object({
57425
- /** Current illuminance in lux (lx). */
57426
- lux: number().min(0).meta({
57427
- unit: "lx",
57839
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57840
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57841
+ precision: 0,
57428
57842
  label: "Illuminance"
57429
- }),
57843
+ })),
57430
57844
  /** Ms epoch when the slice was last updated. */
57431
- lastFetchedAt: number(),
57432
- /** Live display unit from the upstream source (e.g. HA
57433
- * `attributes.unit_of_measurement`). The UI prefers this over the
57434
- * role's canonical unit. Absent → fall back to the canonical unit. */
57435
- unit: string().optional(),
57436
- /** Suggested decimal places for numeric display.
57437
- * Populated live from the upstream source when provided (e.g. HA
57438
- * `attributes.suggested_display_precision`). Falls back to
57439
- * auto-formatting when absent. */
57440
- precision: number().int().min(0).max(10).optional()
57845
+ lastFetchedAt: number()
57441
57846
  });
57442
57847
  var ambientLightSensorCapability = {
57443
57848
  name: "ambient-light-sensor",
@@ -57469,12 +57874,7 @@ var ambientLightSensorCapability = {
57469
57874
  } } },
57470
57875
  /** Clock fields: written, but excluded from the compare that decides
57471
57876
  * whether persisting is worth a SQLite commit. */
57472
- volatileStateFields: ["lastFetchedAt"],
57473
- customize: {
57474
- version: 1,
57475
- advanced: "derive",
57476
- targets: []
57477
- }
57877
+ volatileStateFields: ["lastFetchedAt"]
57478
57878
  };
57479
57879
  /**
57480
57880
  * Per-class audio metrics aggregated over a sliding window.
@@ -57701,10 +58101,7 @@ var BatteryStatusSchema = object({
57701
58101
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57702
58102
  * declines to band a non-finite reading, which is the correct shape.
57703
58103
  */
57704
- percentage: number().min(0).max(100).nullable().meta({
57705
- unit: "%",
57706
- label: "Charge"
57707
- }),
58104
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57708
58105
  /**
57709
58106
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57710
58107
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57731,7 +58128,14 @@ var BatteryStatusSchema = object({
57731
58128
  */
57732
58129
  sleeping: boolean().meta({ label: "Sleeping" }),
57733
58130
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57734
- lastUpdated: number(),
58131
+ lastUpdated: number().meta(readingMeta("none")),
58132
+ /**
58133
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58134
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58135
+ * that only report charge. A second reading on this cap, not a second
58136
+ * `numeric-sensor` on the same device.
58137
+ */
58138
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57735
58139
  /**
57736
58140
  * Ms epoch of the last time the device PROVED it was reachable — a
57737
58141
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57783,7 +58187,6 @@ var BATTERY_DEVICE_TYPES = [
57783
58187
  /** Generic Customize target for the percentage reading (D936). */
57784
58188
  var batteryCustomize = {
57785
58189
  version: 1,
57786
- advanced: "derive",
57787
58190
  targets: [{
57788
58191
  id: "percentage",
57789
58192
  cardinality: "scalar",
@@ -57792,7 +58195,7 @@ var batteryCustomize = {
57792
58195
  stateField: "percentage"
57793
58196
  },
57794
58197
  deviceTypes: BATTERY_DEVICE_TYPES,
57795
- apply: "add-or-replace",
58198
+ apply: "add",
57796
58199
  path: "percentage",
57797
58200
  sources: [{
57798
58201
  kind: "field",
@@ -57913,10 +58316,16 @@ onStatusChanged: { data: object({
57913
58316
  /**
57914
58317
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57915
58318
  */
57916
- history: { fields: { percentage: {
57917
- kind: "gauge",
57918
- unit: "%"
57919
- } } },
58319
+ history: { fields: {
58320
+ percentage: {
58321
+ kind: "gauge",
58322
+ unit: "%"
58323
+ },
58324
+ volts: {
58325
+ kind: "gauge",
58326
+ unit: "V"
58327
+ }
58328
+ } },
57920
58329
  customize: batteryCustomize,
57921
58330
  /** Clock fields: written, but excluded from the compare that decides
57922
58331
  * whether persisting is worth a SQLite commit. */
@@ -57963,12 +58372,7 @@ var binaryCapability = {
57963
58372
  history: { fields: { on: { kind: "boolean" } } },
57964
58373
  /** Clock fields: written, but excluded from the compare that decides
57965
58374
  * whether persisting is worth a SQLite commit. */
57966
- volatileStateFields: ["lastChangedAt"],
57967
- customize: {
57968
- version: 1,
57969
- advanced: "derive",
57970
- targets: []
57971
- }
58375
+ volatileStateFields: ["lastChangedAt"]
57972
58376
  };
57973
58377
  /**
57974
58378
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57984,7 +58388,7 @@ var binaryCapability = {
57984
58388
  */
57985
58389
  var BrightnessStatusSchema = object({
57986
58390
  /** Current level as 0..100 inclusive. Firmware-reported. */
57987
- percentage: number().min(0).max(100),
58391
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57988
58392
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57989
58393
  lastChangedAt: number()
57990
58394
  });
@@ -58438,21 +58842,21 @@ var ClimateControlStatusSchema = object({
58438
58842
  /** Single setpoint in Celsius. Used by single-target modes
58439
58843
  * (heat / cool / dry). Null when the device is in a range mode
58440
58844
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58441
- target: number().nullable(),
58845
+ target: number().nullable().meta(readingMeta("temperature")),
58442
58846
  /** Upper bound of the dual setpoint in Celsius. Populated only
58443
58847
  * in `heat_cool` mode. */
58444
- targetHigh: number().nullable(),
58848
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58445
58849
  /** Lower bound of the dual setpoint in Celsius. Populated only
58446
58850
  * in `heat_cool` mode. */
58447
- targetLow: number().nullable(),
58851
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58448
58852
  /** Target relative humidity (0..100). Null when the device has no
58449
58853
  * humidity control. */
58450
- targetHumidity: number().min(0).max(100).nullable(),
58854
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58451
58855
  /** Current measured temperature in Celsius. Read-only — pushed by
58452
58856
  * the device's internal sensor. */
58453
- currentTemp: number().nullable(),
58857
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58454
58858
  /** Current measured relative humidity (0..100). Read-only. */
58455
- currentHumidity: number().min(0).max(100).nullable(),
58859
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58456
58860
  /** Vertical louver swing on/off. Null when the device has no vertical
58457
58861
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58458
58862
  swingVertical: boolean().nullable(),
@@ -58460,11 +58864,11 @@ var ClimateControlStatusSchema = object({
58460
58864
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58461
58865
  swingHorizontal: boolean().nullable(),
58462
58866
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58463
- minTemp: number().nullable().optional(),
58867
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58464
58868
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58465
- maxTemp: number().nullable().optional(),
58869
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58466
58870
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58467
- tempStep: number().positive().nullable().optional(),
58871
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58468
58872
  /** Ms epoch when the slice was last updated (push or command). */
58469
58873
  lastFetchedAt: number()
58470
58874
  });
@@ -58606,17 +59010,17 @@ var climateControlCapability = {
58606
59010
  * modes to expose in the picker without a slice fetch.
58607
59011
  */
58608
59012
  var RgbTripletSchema = object({
58609
- r: number().int().min(0).max(255),
58610
- g: number().int().min(0).max(255),
58611
- b: number().int().min(0).max(255)
59013
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59014
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59015
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58612
59016
  });
58613
59017
  var HsvTripletSchema = object({
58614
59018
  /** Hue in degrees, 0..360. */
58615
- h: number().min(0).max(360),
59019
+ h: number().min(0).max(360).meta(readingMeta("none")),
58616
59020
  /** Saturation as 0..100 inclusive. */
58617
- s: number().min(0).max(100),
59021
+ s: number().min(0).max(100).meta(readingMeta("none")),
58618
59022
  /** Value/brightness as 0..100 inclusive. */
58619
- v: number().min(0).max(100)
59023
+ v: number().min(0).max(100).meta(readingMeta("none"))
58620
59024
  });
58621
59025
  var ColorInputSchema = discriminatedUnion("mode", [
58622
59026
  object({
@@ -58645,7 +59049,7 @@ var ColorStatusSchema = object({
58645
59049
  /** Populated when `mode === 'hsv'`. */
58646
59050
  hsv: HsvTripletSchema.optional(),
58647
59051
  /** Populated when `mode === 'mired'`. */
58648
- mireds: number().int().optional(),
59052
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58649
59053
  /** Ms epoch of the last operator-driven change. */
58650
59054
  lastChangedAt: number()
58651
59055
  });
@@ -58840,18 +59244,12 @@ _enum([
58840
59244
  "empty",
58841
59245
  "full"
58842
59246
  ]);
58843
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58844
- * each. */
58845
- var ConsumableRemainingUnitSchema = _enum([
58846
- "days",
58847
- "hours",
58848
- "cycles"
58849
- ]);
58850
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58851
- * days). `value` null = unknown (D393). */
59247
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59248
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59249
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58852
59250
  var ConsumableRemainingSchema = object({
58853
- value: number().min(0).nullable(),
58854
- unit: ConsumableRemainingUnitSchema
59251
+ value: number().min(0).nullable().meta(readingMeta("device")),
59252
+ kind: ConsumableRemainingKindSchema
58855
59253
  });
58856
59254
  /** A single consumable item. Either a continuous `level` (remaining
58857
59255
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58864,7 +59262,7 @@ var ConsumableItemSchema = object({
58864
59262
  /** Display name. */
58865
59263
  label: string().min(1),
58866
59264
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58867
- level: number().min(0).max(100).nullable(),
59265
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58868
59266
  /** The provider's discrete verdict when known (binary mode). */
58869
59267
  status: ConsumableStatusSchema.nullable(),
58870
59268
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58946,8 +59344,8 @@ var consumablesCustomize = {
58946
59344
  transforms: [{ kind: "identity" }]
58947
59345
  }],
58948
59346
  literals: [{
58949
- path: "remaining.unit",
58950
- values: [...ConsumableRemainingUnitSchema.options]
59347
+ path: "remaining.kind",
59348
+ values: [...ConsumableRemainingKindSchema.options]
58951
59349
  }]
58952
59350
  },
58953
59351
  {
@@ -59076,7 +59474,6 @@ reset: method(object({
59076
59474
  },
59077
59475
  "remaining.value": {
59078
59476
  kind: "gauge",
59079
- unitFrom: "remaining.unit",
59080
59477
  deadband: 1,
59081
59478
  displayName: "Remaining"
59082
59479
  }
@@ -59146,10 +59543,9 @@ var contactCapability = {
59146
59543
  * (empty array for the other kinds). It lives in the slice because
59147
59544
  * HA `select.*` entities may update their options at runtime.
59148
59545
  *
59149
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59150
- * `kind === 'numeric'` and are absent (undefined) for all other
59151
- * kinds. They are populated live from HA attributes on each state
59152
- * push — consistent with `options` and the sensor-unit approach.
59546
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59547
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59548
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59153
59549
  */
59154
59550
  var ControlStatusSchema = object({
59155
59551
  kind: _enum([
@@ -59161,39 +59557,17 @@ var ControlStatusSchema = object({
59161
59557
  /** Current value. Numeric kind → number; select/text/datetime → string.
59162
59558
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59163
59559
  * or time-only — HA accepts all three forms). */
59164
- value: union([number(), string()]),
59560
+ value: union([number().meta(readingMeta("device")), string()]),
59165
59561
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59166
59562
  options: array(string()),
59167
59563
  /** Ms epoch when the slice was last updated. */
59168
59564
  lastChangedAt: number(),
59169
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59170
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59171
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59172
- unit: string().optional(),
59173
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59174
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59175
- min: number().optional(),
59176
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59177
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59178
- max: number().optional(),
59179
- /** Step increment. Populated live from HA `attributes.step`.
59180
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59181
- step: number().optional(),
59182
- /** Suggested decimal places for numeric display. Populated live from HA
59183
- * `attributes.suggested_display_precision`, or derived from `step` when the
59184
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59185
- * other kinds. Falls back to auto-formatting when absent. */
59186
- precision: number().int().min(0).max(10).optional(),
59187
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59188
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59189
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59190
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59191
- * `has_date`/`has_time` attributes on each push. */
59192
- format: _enum([
59193
- "date",
59194
- "time",
59195
- "datetime"
59196
- ]).optional()
59565
+ /** Minimum allowed value, in the device quantity's base unit. */
59566
+ min: number().optional().meta(readingMeta("device")),
59567
+ /** Maximum allowed value, in the device quantity's base unit. */
59568
+ max: number().optional().meta(readingMeta("device")),
59569
+ /** Step increment, in the device quantity's base unit. */
59570
+ step: number().optional().meta(readingMeta("device"))
59197
59571
  });
59198
59572
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59199
59573
  object({
@@ -59259,9 +59633,9 @@ var CoverStatusSchema = object({
59259
59633
  ]),
59260
59634
  /** 0 = fully closed, 100 = fully open. Null when the device has
59261
59635
  * no intermediate position surface. */
59262
- position: number().min(0).max(100).nullable(),
59636
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59263
59637
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59264
- tiltPosition: number().min(0).max(100).nullable(),
59638
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59265
59639
  /** Ms epoch when the slice was last updated. */
59266
59640
  lastChangedAt: number()
59267
59641
  });
@@ -59364,9 +59738,9 @@ var NormalizedRangeSchema$1 = object({
59364
59738
  var DayNightStatusSchema = object({
59365
59739
  mode: DayNightModeSchema,
59366
59740
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59367
- sensitivity: number().optional(),
59741
+ sensitivity: number().optional().meta(readingMeta("none")),
59368
59742
  /** Delay before the IR-cut filter flips, in seconds. */
59369
- switchDelaySec: number().optional(),
59743
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59370
59744
  lastFetchedAt: number()
59371
59745
  });
59372
59746
  /**
@@ -59584,11 +59958,11 @@ var DishwasherStatusSchema = object({
59584
59958
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59585
59959
  availablePrograms: array(string()),
59586
59960
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59587
- remainingSeconds: number().int().nonnegative().nullable(),
59961
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59588
59962
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59589
- delayedStartSeconds: number().int().nonnegative().nullable(),
59963
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59590
59964
  /** Cycle completion 0..100, or null when not applicable. */
59591
- progressPercent: number().min(0).max(100).nullable(),
59965
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59592
59966
  /** True when the door is reported open; null when the door state is not known. */
59593
59967
  doorOpen: boolean().nullable(),
59594
59968
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59612,7 +59986,7 @@ var DishwasherStatusSchema = object({
59612
59986
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59613
59987
  machineCareDue: boolean().nullable(),
59614
59988
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59615
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59989
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59616
59990
  /** Option states: null when the appliance does not expose the option or has not said. */
59617
59991
  halfLoad: boolean().nullable(),
59618
59992
  hygienePlus: boolean().nullable(),
@@ -59627,7 +60001,7 @@ var DishwasherStatusSchema = object({
59627
60001
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59628
60002
  activeEvents: array(string()),
59629
60003
  /** Numeric error code when the appliance reports one; null otherwise. */
59630
- errorCode: number().nullable(),
60004
+ errorCode: number().nullable().meta(readingMeta("none")),
59631
60005
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59632
60006
  errorLabel: string().nullable(),
59633
60007
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59858,7 +60232,7 @@ var DoorbellStatusSchema = object({
59858
60232
  /** Ms epoch of the last press. null = never observed since this provider started. */
59859
60233
  lastPressedAt: number().nullable(),
59860
60234
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59861
- pressCountSinceStart: number()
60235
+ pressCountSinceStart: number().meta(readingMeta("count"))
59862
60236
  });
59863
60237
  var DoorbellPressEventSchema = object({
59864
60238
  deviceId: number(),
@@ -59923,17 +60297,7 @@ onPressed: {
59923
60297
  history: "none",
59924
60298
  customize: doorbellCustomize
59925
60299
  };
59926
- /**
59927
- * Enum-state sensor — a string value picked from a finite option set.
59928
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59929
- * states, weather conditions, contact-source channels, mode strings).
59930
- * The option set is stable across observations; it is persisted in the
59931
- * device config blob (via the control slice at adoption) and is NOT
59932
- * stored in `sourceInfo`.
59933
- */
59934
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59935
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59936
- var EnumSensorDateTimeFormatSchema = _enum([
60300
+ _enum([
59937
60301
  "date",
59938
60302
  "time",
59939
60303
  "datetime"
@@ -59941,12 +60305,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59941
60305
  var EnumSensorStatusSchema = object({
59942
60306
  value: string(),
59943
60307
  /**
59944
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59945
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59946
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59947
- * a time-only sensor → `'time'`.
60308
+ * Set when `value` is a timestamp rather than an enum state, so history
60309
+ * does not learn ISO strings as states. The display shape (date / time /
60310
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59948
60311
  */
59949
- format: EnumSensorDateTimeFormatSchema.optional(),
60312
+ timestamp: boolean().optional(),
59950
60313
  /** Ms epoch when the slice was last updated. */
59951
60314
  lastFetchedAt: number()
59952
60315
  });
@@ -59973,11 +60336,11 @@ var enumSensorCapability = {
59973
60336
  * History (D850): the value as an `enum` series (an appliance's program or
59974
60337
  * operation state, an HVAC action). The option set is per device, so the
59975
60338
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59976
- * `format` carries a timestamp, not a state, and is not recorded.
60339
+ * `timestamp` carries a clock, not a state, and is not recorded.
59977
60340
  */
59978
60341
  history: { fields: { value: {
59979
60342
  kind: "enum",
59980
- skipWhenSet: "format"
60343
+ skipWhenSet: "timestamp"
59981
60344
  } } },
59982
60345
  /** Clock fields: written, but excluded from the compare that decides
59983
60346
  * whether persisting is worth a SQLite commit. */
@@ -59997,13 +60360,13 @@ var EventFireSchema = object({
59997
60360
  deviceId: number(),
59998
60361
  eventType: string(),
59999
60362
  data: record(string(), unknown()).nullable(),
60000
- timestamp: number(),
60001
- seq: number()
60363
+ timestamp: number().meta(readingMeta("none")),
60364
+ seq: number().meta(readingMeta("count"))
60002
60365
  });
60003
60366
  var EventEmitterStatusSchema = object({
60004
60367
  eventTypes: array(string()),
60005
60368
  lastEvent: EventFireSchema.nullable(),
60006
- eventCountSinceStart: number()
60369
+ eventCountSinceStart: number().meta(readingMeta("count"))
60007
60370
  });
60008
60371
  var eventEmitterCapability = {
60009
60372
  name: "event-emitter",
@@ -60416,10 +60779,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60416
60779
  var FanControlStatusSchema = object({
60417
60780
  /** Active speed as 0..100 inclusive. Null when the device has no
60418
60781
  * speed surface (single-speed fan). */
60419
- percentage: number().min(0).max(100).nullable(),
60782
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60420
60783
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60421
60784
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60422
- percentageStep: number().positive().optional(),
60785
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60423
60786
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60424
60787
  * — empty string when no preset is active or supported. */
60425
60788
  preset: string(),
@@ -60540,7 +60903,7 @@ var FeatureProbeStatusSchema = object({
60540
60903
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60541
60904
  model: string().nullable(),
60542
60905
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60543
- channelCount: number().nullable(),
60906
+ channelCount: number().nullable().meta(readingMeta("count")),
60544
60907
  /**
60545
60908
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60546
60909
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60663,9 +61026,91 @@ var gasCapability = {
60663
61026
  * whether persisting is worth a SQLite commit. */
60664
61027
  volatileStateFields: ["lastChangedAt"]
60665
61028
  };
61029
+ var GroupCommandsSchema = object({
61030
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61031
+ results: array(object({
61032
+ deviceId: number().int().nonnegative(),
61033
+ cap: string().min(1),
61034
+ method: string().min(1),
61035
+ ok: boolean(),
61036
+ error: string().optional(),
61037
+ /** Ms epoch of the attempt. */
61038
+ at: number()
61039
+ })) });
61040
+ var groupCommandsCapability = {
61041
+ name: "group-commands",
61042
+ scope: "device",
61043
+ deviceNative: true,
61044
+ mode: "singleton",
61045
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
61046
+ methods: {},
61047
+ status: {
61048
+ schema: GroupCommandsSchema,
61049
+ kind: "push"
61050
+ },
61051
+ runtimeState: GroupCommandsSchema,
61052
+ /**
61053
+ * Runtime-state durability: **session** — the last attempt. A restored
61054
+ * failure would outlive the command that caused it.
61055
+ *
61056
+ * See `RuntimeStateDurability`. Enforced by
61057
+ * `scripts/check-runtime-state-durability.ts`.
61058
+ */
61059
+ durability: "session",
61060
+ history: "none"
61061
+ };
61062
+ /**
61063
+ * How many members of a helper group answered (D944). Counts are integers,
61064
+ * not measurements: zero included is a real count, never a stand-in reading.
61065
+ * The reading itself stays on the output capability. History is none — the
61066
+ * series that later statistics read is the output cap's own history block.
61067
+ */
61068
+ var GroupStatusSchema = object({
61069
+ /** Members whose current value entered the aggregate. */
61070
+ included: number().int().nonnegative().meta(readingMeta("count", {
61071
+ precision: 0,
61072
+ label: "Reporting"
61073
+ })),
61074
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61075
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61076
+ precision: 0,
61077
+ label: "Excluded"
61078
+ })),
61079
+ /** `included` + `excluded`. */
61080
+ total: number().int().nonnegative().meta(readingMeta("count", {
61081
+ precision: 0,
61082
+ label: "Members"
61083
+ }))
61084
+ });
61085
+ var groupStatusCapability = {
61086
+ name: "group-status",
61087
+ scope: "device",
61088
+ deviceNative: true,
61089
+ mode: "singleton",
61090
+ deviceTypes: [
61091
+ DeviceType.Sensor,
61092
+ DeviceType.Switch,
61093
+ DeviceType.Light
61094
+ ],
61095
+ methods: {},
61096
+ status: {
61097
+ schema: GroupStatusSchema,
61098
+ kind: "push"
61099
+ },
61100
+ runtimeState: GroupStatusSchema,
61101
+ /**
61102
+ * Runtime-state durability: **session** — recomputed from members on the
61103
+ * next source event; a restored count would outlive the readings it named.
61104
+ *
61105
+ * See `RuntimeStateDurability`. Enforced by
61106
+ * `scripts/check-runtime-state-durability.ts`.
61107
+ */
61108
+ durability: "session",
61109
+ history: "none"
61110
+ };
60666
61111
  var HeatingValveStatusSchema = object({
60667
61112
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60668
- position: number().min(0).max(100).nullable(),
61113
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60669
61114
  /** Ms epoch when the slice was last updated (push or poll). */
60670
61115
  lastFetchedAt: number()
60671
61116
  });
@@ -60708,9 +61153,9 @@ var HumidifierStatusSchema = object({
60708
61153
  /** Whether the humidifier is currently on. */
60709
61154
  on: boolean(),
60710
61155
  /** Current measured relative humidity (0..100). Null when not reported. */
60711
- currentHumidity: number().min(0).max(100).nullable(),
61156
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60712
61157
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60713
- targetHumidity: number().min(0).max(100).nullable(),
61158
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60714
61159
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60715
61160
  * device has no mode surface. */
60716
61161
  mode: string().nullable(),
@@ -60720,9 +61165,9 @@ var HumidifierStatusSchema = object({
60720
61165
  * `idle` / `off`). Null when the device doesn't report it. */
60721
61166
  action: string().nullable(),
60722
61167
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60723
- minHumidity: number().nullable(),
61168
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60724
61169
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60725
- maxHumidity: number().nullable(),
61170
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60726
61171
  /** Ms epoch when the slice was last updated. */
60727
61172
  lastChangedAt: number()
60728
61173
  });
@@ -60792,22 +61237,13 @@ var humidifierCapability = {
60792
61237
  * `sourceInfo`.
60793
61238
  */
60794
61239
  var HumiditySensorStatusSchema = object({
60795
- /** Current relative humidity, 0..100. */
60796
- percent: number().min(0).max(100).meta({
60797
- unit: "%",
61240
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61241
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61242
+ precision: 0,
60798
61243
  label: "Humidity"
60799
- }),
61244
+ })),
60800
61245
  /** Ms epoch when the slice was last updated. */
60801
- lastFetchedAt: number(),
60802
- /** Live display unit from the upstream source (e.g. HA
60803
- * `attributes.unit_of_measurement`). The UI prefers this over the
60804
- * role's canonical unit. Absent → fall back to the canonical unit. */
60805
- unit: string().optional(),
60806
- /** Suggested decimal places for numeric display.
60807
- * Populated live from the upstream source when provided (e.g. HA
60808
- * `attributes.suggested_display_precision`). Falls back to
60809
- * auto-formatting when absent. */
60810
- precision: number().int().min(0).max(10).optional()
61246
+ lastFetchedAt: number()
60811
61247
  });
60812
61248
  var humiditySensorCapability = {
60813
61249
  name: "humidity-sensor",
@@ -60838,12 +61274,7 @@ var humiditySensorCapability = {
60838
61274
  } } },
60839
61275
  /** Clock fields: written, but excluded from the compare that decides
60840
61276
  * whether persisting is worth a SQLite commit. */
60841
- volatileStateFields: ["lastFetchedAt"],
60842
- customize: {
60843
- version: 1,
60844
- advanced: "derive",
60845
- targets: []
60846
- }
61277
+ volatileStateFields: ["lastFetchedAt"]
60847
61278
  };
60848
61279
  /**
60849
61280
  * Image display cap. Models a single still image exposed by an integration —
@@ -60864,7 +61295,7 @@ var ImageStatusSchema = object({
60864
61295
  * entity exposes no `entity_picture` (yet). */
60865
61296
  url: string().nullable(),
60866
61297
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60867
- lastUpdated: number().nullable()
61298
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60868
61299
  });
60869
61300
  var imageCapability = {
60870
61301
  name: "image",
@@ -60952,19 +61383,19 @@ var NormalizedRangeSchema = object({
60952
61383
  */
60953
61384
  var ImageSettingsStatusSchema = object({
60954
61385
  /** Normalized 0–100. */
60955
- brightness: number().optional(),
61386
+ brightness: number().optional().meta(readingMeta("none")),
60956
61387
  /** Normalized 0–100. */
60957
- contrast: number().optional(),
61388
+ contrast: number().optional().meta(readingMeta("none")),
60958
61389
  /** Normalized 0–100. */
60959
- saturation: number().optional(),
61390
+ saturation: number().optional().meta(readingMeta("none")),
60960
61391
  /** Normalized 0–100. */
60961
- sharpness: number().optional(),
61392
+ sharpness: number().optional().meta(readingMeta("none")),
60962
61393
  mirror: boolean().optional(),
60963
61394
  flip: boolean().optional(),
60964
61395
  rotate: ImageRotateSchema.optional(),
60965
61396
  whiteBalance: WhiteBalanceModeSchema.optional(),
60966
61397
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60967
- warmth: number().optional(),
61398
+ warmth: number().optional().meta(readingMeta("none")),
60968
61399
  exposureMode: ExposureModeSchema.optional(),
60969
61400
  backlightMode: BacklightModeSchema.optional(),
60970
61401
  lastFetchedAt: number()
@@ -61243,10 +61674,10 @@ var IntercomAbilitySchema = object({
61243
61674
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61244
61675
  codecs: array(string()),
61245
61676
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61246
- sampleRate: number(),
61677
+ sampleRate: number().meta(readingMeta("none")),
61247
61678
  duplex: _enum(["half", "full"]),
61248
61679
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61249
- maxBacklogMs: number()
61680
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61250
61681
  });
61251
61682
  var IntercomStatusSchema = object({
61252
61683
  /** True while a talk session is open. */
@@ -61473,13 +61904,13 @@ var LawnMowerControlStatusSchema = object({
61473
61904
  activity: LawnMowerActivitySchema,
61474
61905
  /** 0..100 battery percentage. Null when the device has no battery
61475
61906
  * reading. */
61476
- batteryLevel: number().min(0).max(100).nullable(),
61907
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61477
61908
  /** 0..100 mowing-completion percentage of the current task, or null when no
61478
61909
  * task is active / progress is unavailable. */
61479
- progressPercent: number().min(0).max(100).nullable(),
61910
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61480
61911
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61481
61912
  * or null when unknown. */
61482
- currentCode: number().nullable(),
61913
+ currentCode: number().nullable().meta(readingMeta("none")),
61483
61914
  /** Human label for {@link currentCode}, or null when undecodable. */
61484
61915
  currentCodeLabel: string().nullable(),
61485
61916
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62012,7 +62443,7 @@ var MediaPlayerStatusSchema = object({
62012
62443
  state: MediaPlayerStateSchema,
62013
62444
  /** Volume as 0..100 inclusive. Null when the device has no volume
62014
62445
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62015
- volumeLevel: number().min(0).max(100).nullable(),
62446
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62016
62447
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62017
62448
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62018
62449
  isMuted: boolean().nullable(),
@@ -62025,10 +62456,10 @@ var MediaPlayerStatusSchema = object({
62025
62456
  currentMedia: MediaInfoSchema.nullable(),
62026
62457
  /** Current playback position in ms. Null when not seekable or
62027
62458
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62028
- positionMs: number().int().nonnegative().nullable(),
62459
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62029
62460
  /** Total duration of the current media in ms. Null when unknown
62030
62461
  * (live stream) or nothing is playing. */
62031
- durationMs: number().int().nonnegative().nullable(),
62462
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62032
62463
  /** Shuffle toggle. Null when no shuffle surface.
62033
62464
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62034
62465
  shuffle: boolean().nullable(),
@@ -62337,7 +62768,7 @@ var motionCustomize = {
62337
62768
  eventName: "onMotionChanged"
62338
62769
  },
62339
62770
  deviceTypes: [DeviceType.Camera],
62340
- apply: "replace",
62771
+ apply: "add",
62341
62772
  applyAuthority: {
62342
62773
  kind: "camera-sensors",
62343
62774
  timeoutField: "motionCooldownMs"
@@ -62409,7 +62840,7 @@ var MotionStatusSchema = object({
62409
62840
  * it, every provider seeded it at boot, and every session left it
62410
62841
  * `detected: true` after its detach.
62411
62842
  */
62412
- autoClearAfterMs: number().nullable()
62843
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62413
62844
  });
62414
62845
  /**
62415
62846
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62446,12 +62877,18 @@ var motionCapability = {
62446
62877
  methods: {
62447
62878
  /**
62448
62879
  * Pull the current motion state synchronously. Convenience shortcut
62449
- * for consumers that don't need the full status object; equivalent
62450
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62451
- * `getStatus` once the auto-injected status surface lands in every
62452
- * consumer.
62880
+ * for consumers that don't need the full status object. A composed
62881
+ * group answers from its slice (D944): unknown throws, it is not off.
62453
62882
  */
62454
62883
  isDetected: method(object({ deviceId: number() }), boolean()) },
62884
+ /**
62885
+ * A composed motion group serves `isDetected` from `detected` (D944).
62886
+ * Without this the cap stays uncomposable (`has-commands`).
62887
+ */
62888
+ composedMethods: { isDetected: {
62889
+ kind: "slice",
62890
+ field: "detected"
62891
+ } },
62455
62892
  events: {
62456
62893
  /**
62457
62894
  * Fires every time the runner transitions a camera between
@@ -62591,7 +63028,7 @@ var MotionZoneRegionSchema = object({
62591
63028
  * the grid region(s). */
62592
63029
  var MotionZoneStatusSchema = object({
62593
63030
  enabled: boolean(),
62594
- sensitivity: number(),
63031
+ sensitivity: number().meta(readingMeta("none")),
62595
63032
  /** Grid region(s). Today exactly one `grid` shape. */
62596
63033
  regions: array(MotionZoneRegionSchema),
62597
63034
  lastFetchedAt: number()
@@ -62961,7 +63398,7 @@ var NavigationStatusSchema = object({
62961
63398
  following: boolean(),
62962
63399
  flash: boolean(),
62963
63400
  lightMode: NavigationLightModeSchema,
62964
- lightLevel: number().min(40).max(100),
63401
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62965
63402
  /** Ms epoch when the slice was last updated. */
62966
63403
  lastChangedAt: number()
62967
63404
  });
@@ -63095,13 +63532,13 @@ var NetworkLinkStatusSchema = object({
63095
63532
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63096
63533
  * SKIP a null rather than coerce it.
63097
63534
  */
63098
- signalPercent: number().min(0).max(100).nullable(),
63535
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63099
63536
  /** Raw received signal strength in dBm, when the firmware reports one. */
63100
- rssiDbm: number().optional(),
63537
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63101
63538
  /** Network name of a wireless link, when the firmware reports it. */
63102
63539
  ssid: string().optional(),
63103
63540
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63104
- lastUpdated: number()
63541
+ lastUpdated: number().meta(readingMeta("none"))
63105
63542
  });
63106
63543
  var networkLinkCapability = {
63107
63544
  name: "network-link",
@@ -63486,7 +63923,7 @@ var NotifierStatusSchema = object({
63486
63923
  lastError: string().nullable(),
63487
63924
  /** Number of deliveries currently buffered server-side (rate-limited
63488
63925
  * notifiers may queue). 0 when the provider sends synchronously. */
63489
- queueDepth: number().int().nonnegative(),
63926
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63490
63927
  /** Per-feature capability matrix — authoritative source for the
63491
63928
  * compose-form field gating. */
63492
63929
  supports: NotifierSupportsSchema
@@ -63578,11 +64015,9 @@ onSent: { data: object({
63578
64015
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63579
64016
  * for: water flow, distance, weight, frequency, signal strength, …).
63580
64017
  *
63581
- * The `unit` and `precision` fields carry the live display metadata that
63582
- * the upstream source provides with each state push (HA
63583
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63584
- * The UI reads them directly from the slice — they are the single source
63585
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64018
+ * The quantity is not on this slice. It is chosen when the device is
64019
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64020
+ * that quantity's base unit. `null` is a refused sample (D393).
63586
64021
  *
63587
64022
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63588
64023
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63591,15 +64026,8 @@ onSent: { data: object({
63591
64026
  * numeric values land here.
63592
64027
  */
63593
64028
  var NumericSensorStatusSchema = object({
63594
- value: number(),
63595
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63596
- * Populated live from the upstream source on each state push.
63597
- * Absent for unitless sensors. */
63598
- unit: string().optional(),
63599
- /** Suggested decimal places for numeric display.
63600
- * Populated live from the upstream source when provided.
63601
- * Falls back to auto-formatting when absent. */
63602
- precision: number().int().min(0).max(10).optional(),
64029
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64030
+ value: number().nullable().meta(readingMeta("device")),
63603
64031
  /** Ms epoch when the slice was last updated. */
63604
64032
  lastFetchedAt: number()
63605
64033
  });
@@ -63623,11 +64051,11 @@ var numericSensorCapability = {
63623
64051
  */
63624
64052
  durability: "restored",
63625
64053
  /**
63626
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64054
+ * History (D850): recorded series. The unit is the device quantity's base,
64055
+ * resolved when the series is read — the cap has no single unit.
63627
64056
  */
63628
64057
  history: { fields: { value: {
63629
64058
  kind: "gauge",
63630
- unitFrom: "unit",
63631
64059
  minIntervalMs: 3e4
63632
64060
  } } },
63633
64061
  /** Clock fields: written, but excluded from the compare that decides
@@ -63919,23 +64347,26 @@ var PetFeederStatusSchema = object({
63919
64347
  /** Food currently in the bowl (grams). Null when the device has not
63920
64348
  * reported a reading yet. On dual-hopper models this is the combined
63921
64349
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63922
- foodLevel: number().nullable(),
64350
+ foodLevel: number().nullable().meta(readingMeta("none")),
63923
64351
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63924
64352
  * single-hopper models. */
63925
- food1: number().nullable(),
64353
+ food1: number().nullable().meta(readingMeta("none")),
63926
64354
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63927
64355
  * single-hopper models. */
63928
- food2: number().nullable(),
64356
+ food2: number().nullable().meta(readingMeta("none")),
63929
64357
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63930
64358
  * (`device_class: problem`, on = low). True when the bowl is empty /
63931
64359
  * below the feeder's low threshold. */
63932
64360
  lowFood: boolean(),
63933
64361
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63934
64362
  * device has no battery reading. */
63935
- batteryPower: number().min(0).max(100).nullable(),
63936
- /** Days of desiccant life remaining. Null when the model has no
63937
- * desiccant sensor. */
63938
- desiccantLeftDays: number().nullable(),
64363
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64364
+ /**
64365
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64366
+ * Providers convert from the days the feeder reports. Null when the
64367
+ * model has no desiccant sensor.
64368
+ */
64369
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63939
64370
  /** True while a feed is in progress. */
63940
64371
  feeding: boolean(),
63941
64372
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63946,7 +64377,7 @@ var PetFeederStatusSchema = object({
63946
64377
  * with `errorCode` for consumers that want the raw integer. */
63947
64378
  error: string().nullable(),
63948
64379
  /** Raw device error code (0 / null = no error). */
63949
- errorCode: number().nullable(),
64380
+ errorCode: number().nullable().meta(readingMeta("none")),
63950
64381
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63951
64382
  isDualHopper: boolean(),
63952
64383
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63956,7 +64387,7 @@ var PetFeederStatusSchema = object({
63956
64387
  /** Play a chime when dispensing. */
63957
64388
  feedSound: boolean(),
63958
64389
  /** Speaker / prompt volume level (device-scaled integer). */
63959
- volume: number(),
64390
+ volume: number().meta(readingMeta("none")),
63960
64391
  /** Ms epoch when the slice was last refreshed from the cloud. */
63961
64392
  lastFetchedAt: number()
63962
64393
  });
@@ -64075,7 +64506,7 @@ var petFeederCapability = {
64075
64506
  },
64076
64507
  desiccantLeftDays: {
64077
64508
  kind: "gauge",
64078
- unit: "days"
64509
+ unit: "s"
64079
64510
  }
64080
64511
  } }
64081
64512
  };
@@ -64476,36 +64907,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64476
64907
  */
64477
64908
  var PowerMeterStatusSchema = object({
64478
64909
  /** Instantaneous power draw in watts. */
64479
- watts: number().optional().meta({
64480
- unit: "W",
64481
- label: "Power"
64482
- }),
64910
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64483
64911
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64484
- kwhTotal: number().optional().meta({
64485
- unit: "kWh",
64486
- label: "Energy"
64487
- }),
64912
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64488
64913
  /** Voltage in volts. */
64489
- volts: number().optional(),
64914
+ volts: number().optional().meta(readingMeta("voltage")),
64490
64915
  /** Current in amperes. */
64491
- amps: number().optional(),
64492
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64493
- va: number().optional(),
64916
+ amps: number().optional().meta(readingMeta("current")),
64917
+ /** Apparent power in volt-amperes. */
64918
+ va: number().optional().meta(readingMeta("apparent-power")),
64494
64919
  /** Power factor (0..1) if reported. */
64495
- powerFactor: number().min(0).max(1).optional(),
64920
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64496
64921
  /** Ms epoch when the slice was last updated. */
64497
- lastFetchedAt: number(),
64498
- /** Live display unit of the single metric this slice carries (e.g. HA
64499
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64500
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64501
- * unit per slice is unambiguous. The UI prefers this over the role's
64502
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64503
- unit: string().optional(),
64504
- /** Suggested decimal places for numeric display.
64505
- * Populated live from the upstream source when provided (e.g. HA
64506
- * `attributes.suggested_display_precision`). Falls back to
64507
- * auto-formatting when absent. */
64508
- precision: number().int().min(0).max(10).optional()
64922
+ lastFetchedAt: number()
64509
64923
  });
64510
64924
  var powerMeterCapability = {
64511
64925
  name: "power-meter",
@@ -64569,12 +64983,7 @@ var powerMeterCapability = {
64569
64983
  } },
64570
64984
  /** Clock fields: written, but excluded from the compare that decides
64571
64985
  * whether persisting is worth a SQLite commit. */
64572
- volatileStateFields: ["lastFetchedAt"],
64573
- customize: {
64574
- version: 1,
64575
- advanced: "derive",
64576
- targets: []
64577
- }
64986
+ volatileStateFields: ["lastFetchedAt"]
64578
64987
  };
64579
64988
  /**
64580
64989
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64594,11 +65003,11 @@ var powerMeterCapability = {
64594
65003
  */
64595
65004
  var GpsLocationSchema = object({
64596
65005
  /** Latitude in decimal degrees, -90..90. */
64597
- latitude: number().min(-90).max(90),
65006
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64598
65007
  /** Longitude in decimal degrees, -180..180. */
64599
- longitude: number().min(-180).max(180),
65008
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64600
65009
  /** Reported accuracy in meters (lower = better). */
64601
- accuracyMeters: number().nonnegative()
65010
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64602
65011
  });
64603
65012
  var PresenceStatusSchema = object({
64604
65013
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64610,7 +65019,7 @@ var PresenceStatusSchema = object({
64610
65019
  gps: GpsLocationSchema.nullable(),
64611
65020
  /** Optional battery level of the tracking device (0..100). Null
64612
65021
  * when not reported. */
64613
- batteryPercent: number().min(0).max(100).nullable(),
65022
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64614
65023
  /** Ms epoch when the slice was last updated. */
64615
65024
  lastChangedAt: number()
64616
65025
  });
@@ -64660,19 +65069,10 @@ var presenceCapability = {
64660
65069
  * `sourceInfo`.
64661
65070
  */
64662
65071
  var PressureSensorStatusSchema = object({
64663
- /** Current pressure in hPa. */
64664
- hpa: number(),
65072
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65073
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64665
65074
  /** Ms epoch when the slice was last updated. */
64666
- lastFetchedAt: number(),
64667
- /** Live display unit from the upstream source (e.g. HA
64668
- * `attributes.unit_of_measurement`). The UI prefers this over the
64669
- * role's canonical unit. Absent → fall back to the canonical unit. */
64670
- unit: string().optional(),
64671
- /** Suggested decimal places for numeric display.
64672
- * Populated live from the upstream source when provided (e.g. HA
64673
- * `attributes.suggested_display_precision`). Falls back to
64674
- * auto-formatting when absent. */
64675
- precision: number().int().min(0).max(10).optional()
65075
+ lastFetchedAt: number()
64676
65076
  });
64677
65077
  var pressureSensorCapability = {
64678
65078
  name: "pressure-sensor",
@@ -65086,8 +65486,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
65086
65486
  }), _void(), { kind: "mutation" });
65087
65487
  var PtzAutotrackSettingsSchema = object({
65088
65488
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65089
- stopDelaySeconds: number().int().min(0).max(300),
65090
- disappearDelaySeconds: number().int().min(0).max(300)
65489
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65490
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65091
65491
  });
65092
65492
  /**
65093
65493
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65241,7 +65641,7 @@ var RunningStateStatusSchema = object({
65241
65641
  /** null = unknown — never coerced to false (D393). */
65242
65642
  running: boolean().nullable(),
65243
65643
  /** Epoch ms the current boolean began; null while `running` is null. */
65244
- since: number().nullable(),
65644
+ since: number().nullable().meta(readingMeta("none")),
65245
65645
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65246
65646
  phase: string().nullable(),
65247
65647
  /** null until a reading has been published (D393). */
@@ -65256,7 +65656,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65256
65656
  /** Generic Customize target for the running boolean (D936). */
65257
65657
  var runningCustomize = {
65258
65658
  version: 1,
65259
- advanced: "derive",
65260
65659
  targets: [{
65261
65660
  id: "running",
65262
65661
  cardinality: "scalar",
@@ -65276,7 +65675,7 @@ var runningCustomize = {
65276
65675
  DeviceType.Thermostat,
65277
65676
  DeviceType.PetFeeder
65278
65677
  ],
65279
- apply: "add-or-replace",
65678
+ apply: "add",
65280
65679
  path: "running",
65281
65680
  sources: [
65282
65681
  {
@@ -66540,9 +66939,9 @@ var RecordTriggerSchema = _enum([
66540
66939
  */
66541
66940
  var RecordWindowSchema = object({
66542
66941
  trigger: RecordTriggerSchema,
66543
- day: number().int().min(0).max(6),
66544
- startMinute: number().int().min(0).max(1439),
66545
- endMinute: number().int().min(1).max(1440)
66942
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66943
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66944
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66546
66945
  });
66547
66946
  /** Status of one physical volume, as the camera itself describes it. */
66548
66947
  var OnboardStorageVolumeSchema = object({
@@ -66618,10 +67017,10 @@ var RecordingOnboardStatusSchema = object({
66618
67017
  /** Master "record to the card at all" switch. */
66619
67018
  enabled: boolean().nullable(),
66620
67019
  overwriteWhenFull: boolean().nullable(),
66621
- preRecordSec: number().nullable(),
66622
- postRecordSec: number().nullable(),
67020
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67021
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66623
67022
  /** Length of one recorded file, in minutes. */
66624
- segmentMinutes: number().nullable(),
67023
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66625
67024
  /** The primary track's weekly windows, flattened. */
66626
67025
  windows: array(RecordWindowSchema),
66627
67026
  /**
@@ -66637,7 +67036,7 @@ var RecordingOnboardStatusSchema = object({
66637
67036
  * provider reporting a non-zero count also reports the schedule as not
66638
67037
  * writable.
66639
67038
  */
66640
- unreadableWindows: number(),
67039
+ unreadableWindows: number().meta(readingMeta("count")),
66641
67040
  /**
66642
67041
  * The camera is scheduled to record and has NO usable storage.
66643
67042
  *
@@ -67482,7 +67881,7 @@ var CamStreamDescriptorSchema = object({
67482
67881
  url: string().optional(),
67483
67882
  codec: string().optional(),
67484
67883
  resolution: CamStreamResolutionSchema.optional(),
67485
- fps: number().positive().optional(),
67884
+ fps: number().positive().optional().meta(readingMeta("rate")),
67486
67885
  label: string().optional(),
67487
67886
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67488
67887
  deviceFeatures: array(string()).optional(),
@@ -67569,18 +67968,18 @@ var StreamProfileSchema = _enum([
67569
67968
  ]);
67570
67969
  /** The current encoding config of one stream profile. */
67571
67970
  var StreamProfileConfigSchema = object({
67572
- width: number(),
67573
- height: number(),
67971
+ width: number().meta(readingMeta("none")),
67972
+ height: number().meta(readingMeta("none")),
67574
67973
  codec: _enum(["h264", "h265"]),
67575
- framerate: number(),
67576
- bitrate: number(),
67974
+ framerate: number().meta(readingMeta("rate")),
67975
+ bitrate: number().meta(readingMeta("bitrate")),
67577
67976
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67578
67977
  encoderProfile: _enum([
67579
67978
  "high",
67580
67979
  "main",
67581
67980
  "baseline"
67582
67981
  ]).optional(),
67583
- gop: number().optional(),
67982
+ gop: number().optional().meta(readingMeta("none")),
67584
67983
  /**
67585
67984
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67586
67985
  *
@@ -67801,12 +68200,7 @@ var switchCapability = {
67801
68200
  label: "On",
67802
68201
  kind: "boolean"
67803
68202
  }
67804
- }] },
67805
- customize: {
67806
- version: 1,
67807
- advanced: "derive",
67808
- targets: []
67809
- }
68203
+ }] }
67810
68204
  };
67811
68205
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67812
68206
  function hasControlCharacter(value) {
@@ -68389,35 +68783,21 @@ var tamperCapability = {
68389
68783
  * entries with `device_class: temperature` and any future native
68390
68784
  * thermometer.
68391
68785
  *
68392
- * Unit normalisation: providers convert to Celsius before storing.
68393
- * The slice value is always Celsius so cross-cap aggregators
68394
- * (climate-control's `currentTemp`, energy analytics) can compose
68395
- * without per-source unit fixups. The canonical display unit (`°C`) is
68396
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68397
- * `sourceInfo`.
68786
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68787
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68398
68788
  *
68399
68789
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68400
68790
  * frozen feed (provider hung) distinct from a "temperature hasn't
68401
68791
  * changed" steady state.
68402
68792
  */
68403
68793
  var TemperatureSensorStatusSchema = object({
68404
- /** Current temperature in Celsius. */
68405
- celsius: number().meta({
68406
- unit: "°C",
68794
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68795
+ celsius: number().nullable().meta(readingMeta("temperature", {
68796
+ precision: 1,
68407
68797
  label: "Temperature"
68408
- }),
68798
+ })),
68409
68799
  /** Ms epoch when the slice was last updated (push or poll). */
68410
- lastFetchedAt: number(),
68411
- /** Live display unit from the upstream source (e.g. HA
68412
- * `attributes.unit_of_measurement`). The UI prefers this over the
68413
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68414
- * Absent → the UI falls back to the role's canonical unit. */
68415
- unit: string().optional(),
68416
- /** Suggested decimal places for numeric display.
68417
- * Populated live from the upstream source when provided (e.g. HA
68418
- * `attributes.suggested_display_precision`). Falls back to
68419
- * auto-formatting when absent. */
68420
- precision: number().int().min(0).max(10).optional()
68800
+ lastFetchedAt: number()
68421
68801
  });
68422
68802
  var temperatureSensorCapability = {
68423
68803
  name: "temperature-sensor",
@@ -68448,12 +68828,7 @@ var temperatureSensorCapability = {
68448
68828
  } } },
68449
68829
  /** Clock fields: written, but excluded from the compare that decides
68450
68830
  * whether persisting is worth a SQLite commit. */
68451
- volatileStateFields: ["lastFetchedAt"],
68452
- customize: {
68453
- version: 1,
68454
- advanced: "derive",
68455
- targets: []
68456
- }
68831
+ volatileStateFields: ["lastFetchedAt"]
68457
68832
  };
68458
68833
  /**
68459
68834
  * toast — system-scoped singleton capability that streams toast
@@ -68736,16 +69111,16 @@ var VacuumControlStatusSchema = object({
68736
69111
  ]),
68737
69112
  /** 0..100 battery percentage. Null when the device has no battery
68738
69113
  * reading. */
68739
- batteryLevel: number().min(0).max(100).nullable(),
69114
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68740
69115
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68741
69116
  * the vacuum has no speed control. */
68742
69117
  fanSpeed: string().nullable(),
68743
69118
  /** Speed tokens the hardware accepts — drives the UI selector. */
68744
69119
  availableFanSpeeds: array(string()),
68745
69120
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68746
- progressPercent: number().min(0).max(100).nullable(),
69121
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68747
69122
  /** Current error code (0 / null = no error). */
68748
- errorCode: number().nullable(),
69123
+ errorCode: number().nullable().meta(readingMeta("none")),
68749
69124
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68750
69125
  errorLabel: string().nullable(),
68751
69126
  /** Ms epoch when the slice was last updated. */
@@ -68823,7 +69198,7 @@ var ValveStatusSchema = object({
68823
69198
  ]),
68824
69199
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68825
69200
  * intermediate position surface. */
68826
- position: number().min(0).max(100).nullable(),
69201
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68827
69202
  /** Ms epoch when the slice was last updated. */
68828
69203
  lastChangedAt: number()
68829
69204
  });
@@ -68931,9 +69306,9 @@ var vibrationCapability = {
68931
69306
  */
68932
69307
  var WaterHeaterStatusSchema = object({
68933
69308
  /** Current measured temperature. Null when not reported. */
68934
- currentTemp: number().nullable(),
69309
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68935
69310
  /** Target temperature setpoint. Null when no setpoint surface. */
68936
- targetTemp: number().nullable(),
69311
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68937
69312
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68938
69313
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68939
69314
  * device reports an unknown state. */
@@ -68944,9 +69319,9 @@ var WaterHeaterStatusSchema = object({
68944
69319
  * has no away surface. */
68945
69320
  away: boolean().nullable(),
68946
69321
  /** HA `min_temp` attribute. Null when not reported. */
68947
- minTemp: number().nullable(),
69322
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68948
69323
  /** HA `max_temp` attribute. Null when not reported. */
68949
- maxTemp: number().nullable(),
69324
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68950
69325
  /** Ms epoch when the slice was last updated. */
68951
69326
  lastChangedAt: number()
68952
69327
  });
@@ -69031,25 +69406,18 @@ var waterHeaterCapability = {
69031
69406
  * Forecast deferred — current conditions only for v1.
69032
69407
  */
69033
69408
  var WeatherStatusSchema = object({
69034
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69035
- * when no condition has been reported yet. */
69409
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69036
69410
  condition: string().nullable(),
69037
- /** Current temperature in the reported unit. Null when not provided. */
69038
- temperature: number().nullable(),
69039
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69040
- temperatureUnit: string().nullable(),
69411
+ /** Current temperature in Celsius. Null when not provided or refused. */
69412
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69041
69413
  /** Relative humidity (0..100). Null when not provided. */
69042
- humidity: number().min(0).max(100).nullable(),
69043
- /** Barometric pressure in the reported unit. Null when not provided. */
69044
- pressure: number().nullable(),
69045
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69046
- pressureUnit: string().nullable(),
69047
- /** Wind speed in the reported unit. Null when not provided. */
69048
- windSpeed: number().nullable(),
69049
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69050
- windSpeedUnit: string().nullable(),
69414
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69415
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69416
+ pressure: number().nullable().meta(readingMeta("pressure")),
69417
+ /** Wind speed in metres per second. Null when not provided or refused. */
69418
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69051
69419
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69052
- windBearing: number().nullable(),
69420
+ windBearing: number().nullable().meta(readingMeta("angle")),
69053
69421
  /** Ms epoch when the slice was last updated. */
69054
69422
  lastFetchedAt: number()
69055
69423
  });
@@ -69077,12 +69445,12 @@ var weatherCapability = {
69077
69445
  */
69078
69446
  durability: "session",
69079
69447
  /**
69080
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69448
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69081
69449
  */
69082
69450
  history: { fields: {
69083
69451
  temperature: {
69084
69452
  kind: "gauge",
69085
- unitFrom: "temperatureUnit",
69453
+ unit: "°C",
69086
69454
  deadband: .2
69087
69455
  },
69088
69456
  humidity: {
@@ -69092,12 +69460,12 @@ var weatherCapability = {
69092
69460
  },
69093
69461
  pressure: {
69094
69462
  kind: "gauge",
69095
- unitFrom: "pressureUnit",
69463
+ unit: "hPa",
69096
69464
  deadband: .5
69097
69465
  },
69098
69466
  windSpeed: {
69099
69467
  kind: "gauge",
69100
- unitFrom: "windSpeedUnit",
69468
+ unit: "m/s",
69101
69469
  deadband: .5
69102
69470
  }
69103
69471
  } }
@@ -70328,6 +70696,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
70328
70696
  featureProbe: featureProbeCapability,
70329
70697
  flood: floodCapability,
70330
70698
  gas: gasCapability,
70699
+ groupCommands: groupCommandsCapability,
70700
+ groupStatus: groupStatusCapability,
70331
70701
  heatingValve: heatingValveCapability,
70332
70702
  humidifier: humidifierCapability,
70333
70703
  humiditySensor: humiditySensorCapability,
@@ -71704,6 +72074,13 @@ Object.freeze({
71704
72074
  access: "view",
71705
72075
  role: "admin"
71706
72076
  },
72077
+ "addons.getPackageDocs": {
72078
+ capName: "addons",
72079
+ capScope: "system",
72080
+ addonId: null,
72081
+ access: "view",
72082
+ role: "admin"
72083
+ },
71707
72084
  "addons.getVersions": {
71708
72085
  capName: "addons",
71709
72086
  capScope: "system",
@@ -74812,6 +75189,20 @@ Object.freeze({
74812
75189
  access: "view",
74813
75190
  role: "viewer"
74814
75191
  },
75192
+ "groupCommands.getStatus": {
75193
+ capName: "group-commands",
75194
+ capScope: "device",
75195
+ addonId: null,
75196
+ access: "view",
75197
+ role: "viewer"
75198
+ },
75199
+ "groupStatus.getStatus": {
75200
+ capName: "group-status",
75201
+ capScope: "device",
75202
+ addonId: null,
75203
+ access: "view",
75204
+ role: "viewer"
75205
+ },
74815
75206
  "heatingValve.getStatus": {
74816
75207
  capName: "heating-valve",
74817
75208
  capScope: "device",
@@ -84634,6 +85025,37 @@ var SENSOR_CAP_VOCABULARY = {
84634
85025
  icon: "flame"
84635
85026
  }]
84636
85027
  },
85028
+ "group-commands": {
85029
+ scope: "device",
85030
+ noun: t("Group command", "Comando di gruppo")
85031
+ },
85032
+ "group-status": {
85033
+ scope: "device",
85034
+ noun: t("Group", "Gruppo"),
85035
+ state: [
85036
+ {
85037
+ kind: "numeric",
85038
+ field: "included",
85039
+ quantity: t("the members reporting", "i membri che rispondono"),
85040
+ label: t("Reporting", "Rispondono"),
85041
+ icon: "users"
85042
+ },
85043
+ {
85044
+ kind: "numeric",
85045
+ field: "excluded",
85046
+ quantity: t("the members left out", "i membri esclusi"),
85047
+ label: t("Excluded", "Esclusi"),
85048
+ icon: "users"
85049
+ },
85050
+ {
85051
+ kind: "numeric",
85052
+ field: "total",
85053
+ quantity: t("the member count", "il numero di membri"),
85054
+ label: t("Members", "Membri"),
85055
+ icon: "sigma"
85056
+ }
85057
+ ]
85058
+ },
84637
85059
  "humidity-sensor": {
84638
85060
  scope: "device",
84639
85061
  noun: t("Humidity", "Umidità"),
@@ -84667,7 +85089,6 @@ var SENSOR_CAP_VOCABULARY = {
84667
85089
  field: "value",
84668
85090
  quantity: t("the reading", "il valore"),
84669
85091
  label: t("Reading", "Valore"),
84670
- unitField: "unit",
84671
85092
  icon: "gauge"
84672
85093
  }]
84673
85094
  },
@@ -84803,7 +85224,7 @@ var SENSOR_CAP_VOCABULARY = {
84803
85224
  field: "temperature",
84804
85225
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84805
85226
  label: t("Temperature", "Temperatura"),
84806
- unitField: "temperatureUnit",
85227
+ unit: "°C",
84807
85228
  icon: "thermometer"
84808
85229
  },
84809
85230
  {
@@ -84819,7 +85240,7 @@ var SENSOR_CAP_VOCABULARY = {
84819
85240
  field: "windSpeed",
84820
85241
  quantity: t("the wind speed", "la velocità del vento"),
84821
85242
  label: t("Wind", "Vento"),
84822
- unitField: "windSpeedUnit",
85243
+ unit: "m/s",
84823
85244
  icon: "wind"
84824
85245
  },
84825
85246
  {
@@ -84827,7 +85248,7 @@ var SENSOR_CAP_VOCABULARY = {
84827
85248
  field: "pressure",
84828
85249
  quantity: t("the air pressure", "la pressione atmosferica"),
84829
85250
  label: t("Pressure", "Pressione"),
84830
- unitField: "pressureUnit",
85251
+ unit: "hPa",
84831
85252
  icon: "gauge"
84832
85253
  }
84833
85254
  ]
@@ -86100,6 +86521,16 @@ Object.freeze({
86100
86521
  form: "single",
86101
86522
  optional: false
86102
86523
  }],
86524
+ "groupCommands.getStatus": [{
86525
+ name: "deviceId",
86526
+ form: "single",
86527
+ optional: false
86528
+ }],
86529
+ "groupStatus.getStatus": [{
86530
+ name: "deviceId",
86531
+ form: "single",
86532
+ optional: false
86533
+ }],
86103
86534
  "heatingValve.getStatus": [{
86104
86535
  name: "deviceId",
86105
86536
  form: "single",
@@ -88084,6 +88515,8 @@ new Map(Object.entries({
88084
88515
  "detection-blind": "device-events",
88085
88516
  "motion-noisy": "device-events",
88086
88517
  "motion-calm": "device-events",
88518
+ "package-llm-silent": "device-events",
88519
+ "package-llm-answered": "device-events",
88087
88520
  "recording-stalled": "device-events",
88088
88521
  "recording-healthy": "device-events",
88089
88522
  "recording-degraded": "device-events",
@@ -88167,6 +88600,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88167
88600
  label: "Motion without a subject",
88168
88601
  kinds: ["motion-noisy", "motion-calm"]
88169
88602
  },
88603
+ {
88604
+ id: "package-llm",
88605
+ family: "device-events",
88606
+ label: "Package confirm",
88607
+ kinds: ["package-llm-silent", "package-llm-answered"]
88608
+ },
88170
88609
  {
88171
88610
  id: "recording",
88172
88611
  family: "device-events",
@@ -88348,6 +88787,8 @@ var DEVICE_EVENTS = [
88348
88787
  "detection-blind",
88349
88788
  "motion-noisy",
88350
88789
  "motion-calm",
88790
+ "package-llm-silent",
88791
+ "package-llm-answered",
88351
88792
  "recording-stalled",
88352
88793
  "recording-healthy",
88353
88794
  "recording-degraded",
@@ -88616,6 +89057,15 @@ new Map([
88616
89057
  snapshotLabel: "Cameras with constant motion now",
88617
89058
  snapshotSection: null
88618
89059
  },
89060
+ {
89061
+ id: "package-llm-confirm",
89062
+ family: "device-events",
89063
+ label: "Package confirm (not answering / answering again)",
89064
+ bad: ["package-llm-silent"],
89065
+ recovery: ["package-llm-answered"],
89066
+ snapshotLabel: "Cameras whose package confirm is not answering",
89067
+ snapshotSection: null
89068
+ },
88619
89069
  {
88620
89070
  id: "recording-health",
88621
89071
  family: "device-events",