@camstack/addon-provider-reolink 1.2.209 → 1.2.211

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.mjs CHANGED
@@ -28,7 +28,7 @@ import { URL as URL$1 } from "url";
28
28
  import { spawn as spawn$1 } from "node:child_process";
29
29
  import os, { tmpdir } from "node:os";
30
30
  import { createReadStream } from "node:fs";
31
- //#region ../types/dist/event-category-DHXxB8t9.mjs
31
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
32
32
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
33
33
  EventCategory["SystemBoot"] = "system.boot";
34
34
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -354,6 +354,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
354
354
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
355
355
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
356
356
  /**
357
+ * A camera's package LLM confirm has failed closed three times in a row
358
+ * (D931, 2026-10-26). One edge per camera until
359
+ * {@link PipelinePackageLlmAnswered}.
360
+ */
361
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
362
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
363
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
364
+ /**
357
365
  * Per-camera pipeline config was mutated by the orchestrator
358
366
  * (a settings change via `setAgentInferenceDevices` /
359
367
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5463,7 +5471,7 @@ var ZodIssueCode = {
5463
5471
  var ZodFirstPartyTypeKind;
5464
5472
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5465
5473
  //#endregion
5466
- //#region ../types/dist/sleep-BErAxhul.mjs
5474
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5467
5475
  /**
5468
5476
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5469
5477
  * window to float samples (D455).
@@ -8021,6 +8029,99 @@ var RecordKindDeclarationSchema = object({
8021
8029
  }).strict();
8022
8030
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8023
8031
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8032
+ var READING_QUANTITIES = [
8033
+ "temperature",
8034
+ "pressure",
8035
+ "speed",
8036
+ "precipitation",
8037
+ "precipitation-rate",
8038
+ "length",
8039
+ "area",
8040
+ "illuminance",
8041
+ "irradiance",
8042
+ "power",
8043
+ "energy",
8044
+ "voltage",
8045
+ "current",
8046
+ "apparent-power",
8047
+ "humidity",
8048
+ "percentage",
8049
+ "concentration-volume",
8050
+ "concentration-mass",
8051
+ "concentration-ppb",
8052
+ "volume",
8053
+ "duration",
8054
+ "duration-ms",
8055
+ "information",
8056
+ "sound-level",
8057
+ "signal-level",
8058
+ "colour-temperature",
8059
+ "angle",
8060
+ "bitrate",
8061
+ "rate",
8062
+ "mass",
8063
+ "index",
8064
+ "ratio",
8065
+ "count",
8066
+ "none",
8067
+ "device"
8068
+ ];
8069
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8070
+ new Set(READING_QUANTITIES);
8071
+ /**
8072
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8073
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8074
+ */
8075
+ var QUANTITY_BASE = {
8076
+ temperature: "°C",
8077
+ pressure: "hPa",
8078
+ speed: "m/s",
8079
+ precipitation: "mm",
8080
+ "precipitation-rate": "mm/h",
8081
+ length: "m",
8082
+ area: "m²",
8083
+ illuminance: "lx",
8084
+ irradiance: "W/m²",
8085
+ power: "W",
8086
+ energy: "kWh",
8087
+ voltage: "V",
8088
+ current: "A",
8089
+ "apparent-power": "VA",
8090
+ humidity: "%",
8091
+ percentage: "%",
8092
+ "concentration-volume": "ppm",
8093
+ "concentration-mass": "µg/m³",
8094
+ "concentration-ppb": "ppb",
8095
+ volume: "L",
8096
+ duration: "s",
8097
+ "duration-ms": "ms",
8098
+ information: "B",
8099
+ "sound-level": "dBFS",
8100
+ "signal-level": "dBm",
8101
+ "colour-temperature": "mired",
8102
+ angle: "°",
8103
+ bitrate: "kbps",
8104
+ rate: "fps",
8105
+ mass: "kg",
8106
+ index: "none",
8107
+ ratio: "none",
8108
+ count: "none",
8109
+ none: "none",
8110
+ device: "device"
8111
+ };
8112
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8113
+ function baseUnitOf(quantity) {
8114
+ const symbol = QUANTITY_BASE[quantity];
8115
+ if (symbol === "none" || symbol === "device") return void 0;
8116
+ return symbol;
8117
+ }
8118
+ function readingMeta(quantity, extras) {
8119
+ return {
8120
+ quantity,
8121
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8122
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8123
+ };
8124
+ }
8024
8125
  /** Shared Zod schemas used across streaming capabilities. */
8025
8126
  var CamProfileSchema = _enum([
8026
8127
  "high",
@@ -8048,8 +8149,8 @@ var CamStreamKindSchema = _enum([
8048
8149
  "derived"
8049
8150
  ]);
8050
8151
  var CamStreamResolutionSchema = object({
8051
- width: number().int().positive(),
8052
- height: number().int().positive()
8152
+ width: number().int().positive().meta(readingMeta("none")),
8153
+ height: number().int().positive().meta(readingMeta("none"))
8053
8154
  });
8054
8155
  var CameraStreamSchema = object({
8055
8156
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8601,6 +8702,8 @@ var CAP_NAMES = Object.freeze([
8601
8702
  "filesystem-browse",
8602
8703
  "flood",
8603
8704
  "gas",
8705
+ "group-commands",
8706
+ "group-status",
8604
8707
  "heating-valve",
8605
8708
  "humidifier",
8606
8709
  "humidity-sensor",
@@ -8730,14 +8833,6 @@ Object.freeze({
8730
8833
  {
8731
8834
  "path": "aqi",
8732
8835
  "valueType": "number"
8733
- },
8734
- {
8735
- "path": "unit",
8736
- "valueType": "string"
8737
- },
8738
- {
8739
- "path": "precision",
8740
- "valueType": "number"
8741
8836
  }
8742
8837
  ],
8743
8838
  "alarm-panel": [
@@ -8773,20 +8868,10 @@ Object.freeze({
8773
8868
  "valueType": "boolean"
8774
8869
  }
8775
8870
  ],
8776
- "ambient-light-sensor": [
8777
- {
8778
- "path": "lux",
8779
- "valueType": "number"
8780
- },
8781
- {
8782
- "path": "unit",
8783
- "valueType": "string"
8784
- },
8785
- {
8786
- "path": "precision",
8787
- "valueType": "number"
8788
- }
8789
- ],
8871
+ "ambient-light-sensor": [{
8872
+ "path": "lux",
8873
+ "valueType": "number"
8874
+ }],
8790
8875
  "audio-metrics": [
8791
8876
  {
8792
8877
  "path": "ts",
@@ -8866,6 +8951,10 @@ Object.freeze({
8866
8951
  "path": "lastUpdated",
8867
8952
  "valueType": "number"
8868
8953
  },
8954
+ {
8955
+ "path": "volts",
8956
+ "valueType": "number"
8957
+ },
8869
8958
  {
8870
8959
  "path": "lastContactAt",
8871
8960
  "valueType": "number"
@@ -9108,10 +9197,6 @@ Object.freeze({
9108
9197
  "datetime"
9109
9198
  ]
9110
9199
  },
9111
- {
9112
- "path": "unit",
9113
- "valueType": "string"
9114
- },
9115
9200
  {
9116
9201
  "path": "min",
9117
9202
  "valueType": "number"
@@ -9123,19 +9208,6 @@ Object.freeze({
9123
9208
  {
9124
9209
  "path": "step",
9125
9210
  "valueType": "number"
9126
- },
9127
- {
9128
- "path": "precision",
9129
- "valueType": "number"
9130
- },
9131
- {
9132
- "path": "format",
9133
- "valueType": "enum",
9134
- "enumValues": [
9135
- "date",
9136
- "time",
9137
- "datetime"
9138
- ]
9139
9211
  }
9140
9212
  ],
9141
9213
  "cover": [
@@ -9376,13 +9448,8 @@ Object.freeze({
9376
9448
  "path": "value",
9377
9449
  "valueType": "string"
9378
9450
  }, {
9379
- "path": "format",
9380
- "valueType": "enum",
9381
- "enumValues": [
9382
- "date",
9383
- "time",
9384
- "datetime"
9385
- ]
9451
+ "path": "timestamp",
9452
+ "valueType": "boolean"
9386
9453
  }],
9387
9454
  "event-emitter": [
9388
9455
  {
@@ -9455,6 +9522,20 @@ Object.freeze({
9455
9522
  "path": "detected",
9456
9523
  "valueType": "boolean"
9457
9524
  }],
9525
+ "group-status": [
9526
+ {
9527
+ "path": "included",
9528
+ "valueType": "number"
9529
+ },
9530
+ {
9531
+ "path": "excluded",
9532
+ "valueType": "number"
9533
+ },
9534
+ {
9535
+ "path": "total",
9536
+ "valueType": "number"
9537
+ }
9538
+ ],
9458
9539
  "heating-valve": [{
9459
9540
  "path": "position",
9460
9541
  "valueType": "number"
@@ -9489,20 +9570,10 @@ Object.freeze({
9489
9570
  "valueType": "number"
9490
9571
  }
9491
9572
  ],
9492
- "humidity-sensor": [
9493
- {
9494
- "path": "percent",
9495
- "valueType": "number"
9496
- },
9497
- {
9498
- "path": "unit",
9499
- "valueType": "string"
9500
- },
9501
- {
9502
- "path": "precision",
9503
- "valueType": "number"
9504
- }
9505
- ],
9573
+ "humidity-sensor": [{
9574
+ "path": "percent",
9575
+ "valueType": "number"
9576
+ }],
9506
9577
  "image": [{
9507
9578
  "path": "url",
9508
9579
  "valueType": "string"
@@ -9841,20 +9912,10 @@ Object.freeze({
9841
9912
  "valueType": "boolean"
9842
9913
  }
9843
9914
  ],
9844
- "numeric-sensor": [
9845
- {
9846
- "path": "value",
9847
- "valueType": "number"
9848
- },
9849
- {
9850
- "path": "unit",
9851
- "valueType": "string"
9852
- },
9853
- {
9854
- "path": "precision",
9855
- "valueType": "number"
9856
- }
9857
- ],
9915
+ "numeric-sensor": [{
9916
+ "path": "value",
9917
+ "valueType": "number"
9918
+ }],
9858
9919
  "pet-feeder": [
9859
9920
  {
9860
9921
  "path": "foodLevel",
@@ -9946,14 +10007,6 @@ Object.freeze({
9946
10007
  {
9947
10008
  "path": "powerFactor",
9948
10009
  "valueType": "number"
9949
- },
9950
- {
9951
- "path": "unit",
9952
- "valueType": "string"
9953
- },
9954
- {
9955
- "path": "precision",
9956
- "valueType": "number"
9957
10010
  }
9958
10011
  ],
9959
10012
  "presence": [
@@ -9982,20 +10035,10 @@ Object.freeze({
9982
10035
  "valueType": "number"
9983
10036
  }
9984
10037
  ],
9985
- "pressure-sensor": [
9986
- {
9987
- "path": "hpa",
9988
- "valueType": "number"
9989
- },
9990
- {
9991
- "path": "unit",
9992
- "valueType": "string"
9993
- },
9994
- {
9995
- "path": "precision",
9996
- "valueType": "number"
9997
- }
9998
- ],
10038
+ "pressure-sensor": [{
10039
+ "path": "hpa",
10040
+ "valueType": "number"
10041
+ }],
9999
10042
  "privacy-mask": [{
10000
10043
  "path": "enabled",
10001
10044
  "valueType": "boolean"
@@ -10306,20 +10349,10 @@ Object.freeze({
10306
10349
  "path": "tampered",
10307
10350
  "valueType": "boolean"
10308
10351
  }],
10309
- "temperature-sensor": [
10310
- {
10311
- "path": "celsius",
10312
- "valueType": "number"
10313
- },
10314
- {
10315
- "path": "unit",
10316
- "valueType": "string"
10317
- },
10318
- {
10319
- "path": "precision",
10320
- "valueType": "number"
10321
- }
10322
- ],
10352
+ "temperature-sensor": [{
10353
+ "path": "celsius",
10354
+ "valueType": "number"
10355
+ }],
10323
10356
  "update": [
10324
10357
  {
10325
10358
  "path": "currentVersion",
@@ -10430,10 +10463,6 @@ Object.freeze({
10430
10463
  "path": "temperature",
10431
10464
  "valueType": "number"
10432
10465
  },
10433
- {
10434
- "path": "temperatureUnit",
10435
- "valueType": "string"
10436
- },
10437
10466
  {
10438
10467
  "path": "humidity",
10439
10468
  "valueType": "number"
@@ -10442,18 +10471,10 @@ Object.freeze({
10442
10471
  "path": "pressure",
10443
10472
  "valueType": "number"
10444
10473
  },
10445
- {
10446
- "path": "pressureUnit",
10447
- "valueType": "string"
10448
- },
10449
10474
  {
10450
10475
  "path": "windSpeed",
10451
10476
  "valueType": "number"
10452
10477
  },
10453
- {
10454
- "path": "windSpeedUnit",
10455
- "valueType": "string"
10456
- },
10457
10478
  {
10458
10479
  "path": "windBearing",
10459
10480
  "valueType": "number"
@@ -10884,7 +10905,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10884
10905
  return DeviceRole;
10885
10906
  }({});
10886
10907
  //#endregion
10887
- //#region ../types/dist/composition-kEl9mevu.mjs
10908
+ //#region ../types/dist/composition-OhpuespA.mjs
10888
10909
  /**
10889
10910
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10890
10911
  *
@@ -11639,15 +11660,74 @@ function validateExpressionSource(src) {
11639
11660
  }
11640
11661
  return null;
11641
11662
  }
11642
- /** A composed device's stableId is this prefix plus the block id. */
11643
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11644
- /** Longest snippet a `code` source may carry (slice 4). */
11645
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11663
+ /**
11664
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11665
+ * id. Split out so helper schemas can name it without a cycle through
11666
+ * `composition.ts` (D659).
11667
+ */
11646
11668
  var CompositionSourceRefSchema = object({
11647
11669
  /** The owning addon; `stableId` is unique only within it. */
11648
11670
  addonId: string().min(1),
11649
11671
  stableId: string().min(1)
11650
11672
  });
11673
+ var GROUP_TEMPLATE_IDS = [
11674
+ "temperature",
11675
+ "humidity",
11676
+ "pressure",
11677
+ "illuminance",
11678
+ "power",
11679
+ "energy",
11680
+ "battery",
11681
+ "numeric",
11682
+ "motion",
11683
+ "contact",
11684
+ "leak",
11685
+ "smoke",
11686
+ "occupancy",
11687
+ "binary",
11688
+ "switch",
11689
+ "light"
11690
+ ];
11691
+ var NUMERIC_AGGREGATES = [
11692
+ "mean",
11693
+ "median",
11694
+ "min",
11695
+ "max",
11696
+ "sum"
11697
+ ];
11698
+ var BINARY_AGGREGATES = ["any", "all"];
11699
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11700
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11701
+ var AGGREGATE_REDUCERS = [
11702
+ "mean",
11703
+ "median",
11704
+ "min",
11705
+ "max",
11706
+ "sum",
11707
+ "any",
11708
+ "all"
11709
+ ];
11710
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11711
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11712
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11713
+ /** One member. The field is stored so occupancy can name motion or binary. */
11714
+ var HelperMemberSchema = object({
11715
+ source: CompositionSourceRefSchema,
11716
+ cap: string().min(1),
11717
+ fieldPath: string().min(1)
11718
+ });
11719
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11720
+ kind: literal("group"),
11721
+ templateId: GroupTemplateIdSchema,
11722
+ aggregate: AggregateReducerSchema,
11723
+ members: array(HelperMemberSchema).min(1).max(64),
11724
+ /** Actuator groups only. Readings leave this unset. */
11725
+ active: ActiveCriterionSchema.optional()
11726
+ })]);
11727
+ /** A composed device's stableId is this prefix plus the block id. */
11728
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11729
+ /** Longest snippet a `code` source may carry (slice 4). */
11730
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11651
11731
  /** One field of one capability of one source device. */
11652
11732
  var CompositionFieldReadSchema = object({
11653
11733
  source: CompositionSourceRefSchema,
@@ -11691,7 +11771,33 @@ var CompositionCodeSourceSchema = object({
11691
11771
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11692
11772
  CompositionFromSourceSchema,
11693
11773
  CompositionExpressionSourceSchema,
11694
- CompositionCodeSourceSchema
11774
+ CompositionCodeSourceSchema,
11775
+ object({
11776
+ kind: literal("aggregate"),
11777
+ reducer: _enum([
11778
+ "mean",
11779
+ "median",
11780
+ "min",
11781
+ "max",
11782
+ "sum",
11783
+ "any",
11784
+ "all"
11785
+ ]),
11786
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11787
+ /** `value` is the reading; the others are the member counts. */
11788
+ publish: _enum([
11789
+ "value",
11790
+ "included",
11791
+ "excluded",
11792
+ "total"
11793
+ ]),
11794
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11795
+ empty: _enum([
11796
+ "null",
11797
+ "omit",
11798
+ "offline"
11799
+ ])
11800
+ })
11695
11801
  ]);
11696
11802
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11697
11803
  kind: literal("forward"),
@@ -11737,7 +11843,13 @@ var CompositionSchema = object({
11737
11843
  kind: literal("existing"),
11738
11844
  device: CompositionSourceRefSchema
11739
11845
  })]),
11740
- features: array(CompositionFeatureSchema).min(1).max(16)
11846
+ features: array(CompositionFeatureSchema).min(1).max(16),
11847
+ /**
11848
+ * Present when the block is a helper template. Absent on a free
11849
+ * composition (the room example, device 4603). The editor reads this,
11850
+ * never the compiled features.
11851
+ */
11852
+ helper: HelperSpecSchema.optional()
11741
11853
  }).superRefine((composition, ctx) => {
11742
11854
  const seen = /* @__PURE__ */ new Set();
11743
11855
  composition.features.forEach((feature, index) => {
@@ -18515,7 +18627,11 @@ var CompositionProblemSchema = object({
18515
18627
  "item-field-reserved",
18516
18628
  "target-missing",
18517
18629
  "target-unreadable",
18518
- "not-replaceable"
18630
+ "not-replaceable",
18631
+ "native-capability-replace",
18632
+ "mixed-quantity",
18633
+ "helper-mismatch",
18634
+ "member-constraint"
18519
18635
  ]),
18520
18636
  path: string(),
18521
18637
  message: string()
@@ -18536,7 +18652,8 @@ var CompositionUnavailableModeSchema = _enum([
18536
18652
  "null",
18537
18653
  "offline",
18538
18654
  "unknown",
18539
- "release"
18655
+ "release",
18656
+ "omit"
18540
18657
  ]);
18541
18658
  /**
18542
18659
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18573,9 +18690,11 @@ var CompositionFieldPlanSchema = object({
18573
18690
  reads: array(CompositionFieldReadSchema)
18574
18691
  });
18575
18692
  /**
18576
- * What a feature does to its target (D663): `new` builds a capability on a new
18577
- * device; on an existing device it `add`s a capability the device lacks, or
18578
- * `replace`s named fields of one it has natively.
18693
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18694
+ * new device; on an existing device it `add`s a capability the device lacks.
18695
+ * Replacing a capability the device already serves is refused
18696
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18697
+ * wire for a claim the hub already holds.
18579
18698
  */
18580
18699
  var CompositionFeatureModeSchema = _enum([
18581
18700
  "new",
@@ -18748,6 +18867,275 @@ Object.freeze({
18748
18867
  result: ["number", "null"]
18749
18868
  }
18750
18869
  });
18870
+ /**
18871
+ * One catalog row per base reading a group helper can build (D944 §3).
18872
+ * Data, not a form default written into the block.
18873
+ */
18874
+ var NOW = {
18875
+ fieldPath: "lastFetchedAt",
18876
+ expr: "now"
18877
+ };
18878
+ var CHANGED = {
18879
+ fieldPath: "lastChangedAt",
18880
+ expr: "now"
18881
+ };
18882
+ function numeric(input) {
18883
+ return {
18884
+ ...input,
18885
+ family: "numeric",
18886
+ deviceType: DeviceType.Sensor,
18887
+ memberDeviceType: DeviceType.Sensor,
18888
+ commandCaps: [],
18889
+ baseUnit: baseUnitOf(input.quantity),
18890
+ aggregates: NUMERIC_AGGREGATES,
18891
+ accept: [{
18892
+ cap: input.outputCap,
18893
+ fieldPath: input.outputField
18894
+ }]
18895
+ };
18896
+ }
18897
+ function binary(input) {
18898
+ return {
18899
+ ...input,
18900
+ family: "binary",
18901
+ deviceType: DeviceType.Sensor,
18902
+ memberDeviceType: DeviceType.Sensor,
18903
+ commandCaps: [],
18904
+ quantity: "none",
18905
+ baseUnit: void 0,
18906
+ aggregates: BINARY_AGGREGATES,
18907
+ empty: "offline"
18908
+ };
18909
+ }
18910
+ var GROUP_TEMPLATES = [
18911
+ numeric({
18912
+ id: "temperature",
18913
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18914
+ role: DeviceRole.TemperatureSensor,
18915
+ outputCap: "temperature-sensor",
18916
+ outputField: "celsius",
18917
+ quantity: "temperature",
18918
+ defaultAggregate: "mean",
18919
+ empty: "null",
18920
+ companions: [NOW]
18921
+ }),
18922
+ numeric({
18923
+ id: "humidity",
18924
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18925
+ role: DeviceRole.HumiditySensor,
18926
+ outputCap: "humidity-sensor",
18927
+ outputField: "percent",
18928
+ quantity: "humidity",
18929
+ defaultAggregate: "mean",
18930
+ empty: "null",
18931
+ companions: [NOW]
18932
+ }),
18933
+ numeric({
18934
+ id: "pressure",
18935
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18936
+ role: DeviceRole.PressureSensor,
18937
+ outputCap: "pressure-sensor",
18938
+ outputField: "hpa",
18939
+ quantity: "pressure",
18940
+ defaultAggregate: "mean",
18941
+ empty: "null",
18942
+ companions: [NOW]
18943
+ }),
18944
+ numeric({
18945
+ id: "illuminance",
18946
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18947
+ role: DeviceRole.AmbientLightSensor,
18948
+ outputCap: "ambient-light-sensor",
18949
+ outputField: "lux",
18950
+ quantity: "illuminance",
18951
+ defaultAggregate: "mean",
18952
+ empty: "null",
18953
+ companions: [NOW]
18954
+ }),
18955
+ numeric({
18956
+ id: "power",
18957
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18958
+ role: DeviceRole.PowerSensor,
18959
+ outputCap: "power-meter",
18960
+ outputField: "watts",
18961
+ quantity: "power",
18962
+ defaultAggregate: "sum",
18963
+ empty: "omit",
18964
+ companions: [NOW]
18965
+ }),
18966
+ numeric({
18967
+ id: "energy",
18968
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18969
+ role: DeviceRole.EnergySensor,
18970
+ outputCap: "power-meter",
18971
+ outputField: "kwhTotal",
18972
+ quantity: "energy",
18973
+ defaultAggregate: "sum",
18974
+ empty: "omit",
18975
+ companions: [NOW]
18976
+ }),
18977
+ numeric({
18978
+ id: "battery",
18979
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18980
+ role: DeviceRole.BatterySensor,
18981
+ outputCap: "battery",
18982
+ outputField: "percentage",
18983
+ quantity: "percentage",
18984
+ defaultAggregate: "mean",
18985
+ empty: "null",
18986
+ companions: [
18987
+ {
18988
+ fieldPath: "charging",
18989
+ expr: "\"unknown\""
18990
+ },
18991
+ {
18992
+ fieldPath: "sleeping",
18993
+ expr: "false"
18994
+ },
18995
+ {
18996
+ fieldPath: "lastUpdated",
18997
+ expr: "now"
18998
+ }
18999
+ ]
19000
+ }),
19001
+ numeric({
19002
+ id: "numeric",
19003
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19004
+ role: DeviceRole.NumericSensor,
19005
+ outputCap: "numeric-sensor",
19006
+ outputField: "value",
19007
+ quantity: "device",
19008
+ defaultAggregate: "mean",
19009
+ empty: "null",
19010
+ companions: [NOW]
19011
+ }),
19012
+ binary({
19013
+ id: "motion",
19014
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19015
+ role: DeviceRole.MotionSensor,
19016
+ outputCap: "motion",
19017
+ outputField: "detected",
19018
+ defaultAggregate: "any",
19019
+ accept: [{
19020
+ cap: "motion",
19021
+ fieldPath: "detected"
19022
+ }],
19023
+ companions: [{
19024
+ fieldPath: "lastDetectedAt",
19025
+ expr: "null"
19026
+ }, {
19027
+ fieldPath: "autoClearAfterMs",
19028
+ expr: "null"
19029
+ }]
19030
+ }),
19031
+ binary({
19032
+ id: "contact",
19033
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19034
+ role: DeviceRole.ContactSensor,
19035
+ outputCap: "contact",
19036
+ outputField: "entryOpen",
19037
+ defaultAggregate: "any",
19038
+ accept: [{
19039
+ cap: "contact",
19040
+ fieldPath: "entryOpen"
19041
+ }],
19042
+ companions: [CHANGED]
19043
+ }),
19044
+ binary({
19045
+ id: "leak",
19046
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19047
+ role: DeviceRole.LeakSensor,
19048
+ outputCap: "flood",
19049
+ outputField: "flooded",
19050
+ defaultAggregate: "any",
19051
+ accept: [{
19052
+ cap: "flood",
19053
+ fieldPath: "flooded"
19054
+ }],
19055
+ companions: [CHANGED]
19056
+ }),
19057
+ binary({
19058
+ id: "smoke",
19059
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19060
+ role: DeviceRole.SmokeSensor,
19061
+ outputCap: "smoke",
19062
+ outputField: "detected",
19063
+ defaultAggregate: "any",
19064
+ accept: [{
19065
+ cap: "smoke",
19066
+ fieldPath: "detected"
19067
+ }],
19068
+ companions: [CHANGED]
19069
+ }),
19070
+ binary({
19071
+ id: "occupancy",
19072
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19073
+ role: DeviceRole.BinarySensor,
19074
+ outputCap: "binary",
19075
+ outputField: "on",
19076
+ defaultAggregate: "any",
19077
+ accept: [{
19078
+ cap: "motion",
19079
+ fieldPath: "detected"
19080
+ }, {
19081
+ cap: "binary",
19082
+ fieldPath: "on"
19083
+ }],
19084
+ companions: [CHANGED]
19085
+ }),
19086
+ binary({
19087
+ id: "binary",
19088
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19089
+ role: DeviceRole.BinarySensor,
19090
+ outputCap: "binary",
19091
+ outputField: "on",
19092
+ defaultAggregate: "any",
19093
+ accept: [{
19094
+ cap: "binary",
19095
+ fieldPath: "on"
19096
+ }],
19097
+ companions: [CHANGED]
19098
+ }),
19099
+ actuator({
19100
+ id: "switch",
19101
+ 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.",
19102
+ deviceType: DeviceType.Switch,
19103
+ outputCap: "switch",
19104
+ commandCaps: ["switch"]
19105
+ }),
19106
+ actuator({
19107
+ id: "light",
19108
+ 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.",
19109
+ deviceType: DeviceType.Light,
19110
+ outputCap: "switch",
19111
+ commandCaps: [
19112
+ "switch",
19113
+ "brightness",
19114
+ "color"
19115
+ ]
19116
+ })
19117
+ ];
19118
+ function actuator(input) {
19119
+ return {
19120
+ ...input,
19121
+ family: "actuator",
19122
+ role: void 0,
19123
+ memberDeviceType: input.deviceType,
19124
+ outputField: "on",
19125
+ quantity: "none",
19126
+ baseUnit: void 0,
19127
+ aggregates: BINARY_AGGREGATES,
19128
+ defaultAggregate: "any",
19129
+ empty: "offline",
19130
+ accept: [{
19131
+ cap: "switch",
19132
+ fieldPath: "on"
19133
+ }],
19134
+ companions: [CHANGED]
19135
+ };
19136
+ }
19137
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19138
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18751
19139
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18752
19140
  /**
18753
19141
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20574,12 +20962,12 @@ function buildDeviceAdminUrl(parts) {
20574
20962
  * providers detect renames by matching `uniqueId` and patch `id`
20575
20963
  * in-place so dispatch keeps working.
20576
20964
  *
20577
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20578
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20579
- * state push and are the single source of truth. The `device_class` tag is
20580
- * an HA-specific input that is consumed at the discovery boundary
20581
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20582
- * stored after that point.
20965
+ * **Display unit and precision are NOT stored here.** They are a read-time
20966
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20967
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20968
+ * neither is repeated on each sample. The `device_class` tag is an
20969
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20970
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20583
20971
  *
20584
20972
  * The `raw` escape hatch carries provider-specific attributes that don't
20585
20973
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20593,6 +20981,14 @@ var SourceInfoSchema = object({
20593
20981
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20594
20982
  * Used to detect rename when `id` changes. */
20595
20983
  uniqueId: string().optional(),
20984
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20985
+ quantity: ReadingQuantitySchema.optional(),
20986
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20987
+ dateTimeShape: _enum([
20988
+ "date",
20989
+ "time",
20990
+ "datetime"
20991
+ ]).optional(),
20596
20992
  /** Provider-specific extras that don't fit the structured slots.
20597
20993
  * Whitelist what goes here per provider — do NOT dump entire HA
20598
20994
  * attribute blobs (would explode the meta JSON column). */
@@ -41697,7 +42093,8 @@ method(object({
41697
42093
  "enum"
41698
42094
  ]),
41699
42095
  enumValues: array(string()).optional(),
41700
- item: boolean().optional()
42096
+ item: boolean().optional(),
42097
+ quantity: ReadingQuantitySchema.optional()
41701
42098
  })).readonly(),
41702
42099
  itemArray: object({
41703
42100
  path: string(),
@@ -45581,8 +45978,8 @@ var MaskPolygonShapeSchema = object({
45581
45978
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45582
45979
  var MaskGridShapeSchema = object({
45583
45980
  kind: literal("grid"),
45584
- gridWidth: number(),
45585
- gridHeight: number(),
45981
+ gridWidth: number().meta(readingMeta("none")),
45982
+ gridHeight: number().meta(readingMeta("none")),
45586
45983
  cells: array(boolean())
45587
45984
  });
45588
45985
  discriminatedUnion("kind", [
@@ -45694,6 +46091,8 @@ var NcSystemEventKindSchema = _enum([
45694
46091
  "detection-blind",
45695
46092
  "motion-noisy",
45696
46093
  "motion-calm",
46094
+ "package-llm-silent",
46095
+ "package-llm-answered",
45697
46096
  "recording-stalled",
45698
46097
  "recording-healthy",
45699
46098
  "recording-degraded",
@@ -57307,6 +57706,28 @@ var PackageVersionInfoSchema = object({
57307
57706
  deprecated: string().optional(),
57308
57707
  distTags: array(string()).readonly()
57309
57708
  });
57709
+ /**
57710
+ * One published document. `ok` is markdown from the registry. `missing` means
57711
+ * the registry answered and the file was not there. `error` is a registry or
57712
+ * tarball failure — never an empty string standing in for either.
57713
+ */
57714
+ var PackageDocBodySchema = discriminatedUnion("status", [
57715
+ object({
57716
+ status: literal("ok"),
57717
+ markdown: string()
57718
+ }),
57719
+ object({ status: literal("missing") }),
57720
+ object({
57721
+ status: literal("error"),
57722
+ message: string()
57723
+ })
57724
+ ]);
57725
+ var PackageDocsSchema = object({
57726
+ name: string(),
57727
+ version: string(),
57728
+ readme: PackageDocBodySchema,
57729
+ changelog: PackageDocBodySchema
57730
+ });
57310
57731
  var SearchResultSchema = object({
57311
57732
  name: string(),
57312
57733
  version: string(),
@@ -57525,7 +57946,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57525
57946
  }), object({ success: literal(true) }), {
57526
57947
  kind: "mutation",
57527
57948
  auth: "admin"
57528
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57949
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57950
+ name: string().min(1),
57951
+ version: string().min(1).optional()
57952
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57529
57953
  kind: "mutation",
57530
57954
  auth: "admin"
57531
57955
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57584,27 +58008,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57584
58008
  */
57585
58009
  var AirQualitySensorStatusSchema = object({
57586
58010
  /** Carbon dioxide concentration in ppm. */
57587
- co2Ppm: number().min(0).optional(),
58011
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57588
58012
  /** Total volatile organic compounds in ppb. */
57589
- vocPpb: number().min(0).optional(),
58013
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57590
58014
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57591
- pm25: number().min(0).optional(),
58015
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57592
58016
  /** Particulate matter ≤ 10 μm in µg/m³. */
57593
- pm10: number().min(0).optional(),
58017
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57594
58018
  /** Composite AQI value (typically 0..500). */
57595
- aqi: number().optional(),
58019
+ aqi: number().optional().meta(readingMeta("index")),
57596
58020
  /** Ms epoch when the slice was last updated. */
57597
- lastFetchedAt: number(),
57598
- /** Live display unit of the single metric this slice carries (e.g. HA
57599
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57600
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57601
- * per slice is unambiguous. */
57602
- unit: string().optional(),
57603
- /** Suggested decimal places for numeric display.
57604
- * Populated live from the upstream source when provided (e.g. HA
57605
- * `attributes.suggested_display_precision`). Falls back to
57606
- * auto-formatting when absent. */
57607
- precision: number().int().min(0).max(10).optional()
58021
+ lastFetchedAt: number()
57608
58022
  });
57609
58023
  var airQualitySensorCapability = {
57610
58024
  name: "air-quality-sensor",
@@ -57641,12 +58055,12 @@ var airQualitySensorCapability = {
57641
58055
  },
57642
58056
  pm25: {
57643
58057
  kind: "gauge",
57644
- unit: "ug/m3",
58058
+ unit: "µg/m³",
57645
58059
  deadband: 1
57646
58060
  },
57647
58061
  pm10: {
57648
58062
  kind: "gauge",
57649
- unit: "ug/m3",
58063
+ unit: "µg/m³",
57650
58064
  deadband: 1
57651
58065
  },
57652
58066
  aqi: {
@@ -57664,22 +58078,13 @@ var airQualitySensorCapability = {
57664
58078
  * entries with `device_class: illuminance`.
57665
58079
  */
57666
58080
  var AmbientLightSensorStatusSchema = object({
57667
- /** Current illuminance in lux (lx). */
57668
- lux: number().min(0).meta({
57669
- unit: "lx",
58081
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
58082
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
58083
+ precision: 0,
57670
58084
  label: "Illuminance"
57671
- }),
58085
+ })),
57672
58086
  /** Ms epoch when the slice was last updated. */
57673
- lastFetchedAt: number(),
57674
- /** Live display unit from the upstream source (e.g. HA
57675
- * `attributes.unit_of_measurement`). The UI prefers this over the
57676
- * role's canonical unit. Absent → fall back to the canonical unit. */
57677
- unit: string().optional(),
57678
- /** Suggested decimal places for numeric display.
57679
- * Populated live from the upstream source when provided (e.g. HA
57680
- * `attributes.suggested_display_precision`). Falls back to
57681
- * auto-formatting when absent. */
57682
- precision: number().int().min(0).max(10).optional()
58087
+ lastFetchedAt: number()
57683
58088
  });
57684
58089
  var ambientLightSensorCapability = {
57685
58090
  name: "ambient-light-sensor",
@@ -57711,12 +58116,7 @@ var ambientLightSensorCapability = {
57711
58116
  } } },
57712
58117
  /** Clock fields: written, but excluded from the compare that decides
57713
58118
  * whether persisting is worth a SQLite commit. */
57714
- volatileStateFields: ["lastFetchedAt"],
57715
- customize: {
57716
- version: 1,
57717
- advanced: "derive",
57718
- targets: []
57719
- }
58119
+ volatileStateFields: ["lastFetchedAt"]
57720
58120
  };
57721
58121
  /**
57722
58122
  * Per-class audio metrics aggregated over a sliding window.
@@ -57943,10 +58343,7 @@ var BatteryStatusSchema = object({
57943
58343
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57944
58344
  * declines to band a non-finite reading, which is the correct shape.
57945
58345
  */
57946
- percentage: number().min(0).max(100).nullable().meta({
57947
- unit: "%",
57948
- label: "Charge"
57949
- }),
58346
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57950
58347
  /**
57951
58348
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57952
58349
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57973,7 +58370,14 @@ var BatteryStatusSchema = object({
57973
58370
  */
57974
58371
  sleeping: boolean().meta({ label: "Sleeping" }),
57975
58372
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57976
- lastUpdated: number(),
58373
+ lastUpdated: number().meta(readingMeta("none")),
58374
+ /**
58375
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58376
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58377
+ * that only report charge. A second reading on this cap, not a second
58378
+ * `numeric-sensor` on the same device.
58379
+ */
58380
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57977
58381
  /**
57978
58382
  * Ms epoch of the last time the device PROVED it was reachable — a
57979
58383
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -58025,7 +58429,6 @@ var BATTERY_DEVICE_TYPES = [
58025
58429
  /** Generic Customize target for the percentage reading (D936). */
58026
58430
  var batteryCustomize = {
58027
58431
  version: 1,
58028
- advanced: "derive",
58029
58432
  targets: [{
58030
58433
  id: "percentage",
58031
58434
  cardinality: "scalar",
@@ -58034,7 +58437,7 @@ var batteryCustomize = {
58034
58437
  stateField: "percentage"
58035
58438
  },
58036
58439
  deviceTypes: BATTERY_DEVICE_TYPES,
58037
- apply: "add-or-replace",
58440
+ apply: "add",
58038
58441
  path: "percentage",
58039
58442
  sources: [{
58040
58443
  kind: "field",
@@ -58155,10 +58558,16 @@ onStatusChanged: { data: object({
58155
58558
  /**
58156
58559
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
58157
58560
  */
58158
- history: { fields: { percentage: {
58159
- kind: "gauge",
58160
- unit: "%"
58161
- } } },
58561
+ history: { fields: {
58562
+ percentage: {
58563
+ kind: "gauge",
58564
+ unit: "%"
58565
+ },
58566
+ volts: {
58567
+ kind: "gauge",
58568
+ unit: "V"
58569
+ }
58570
+ } },
58162
58571
  customize: batteryCustomize,
58163
58572
  /** Clock fields: written, but excluded from the compare that decides
58164
58573
  * whether persisting is worth a SQLite commit. */
@@ -58205,12 +58614,7 @@ var binaryCapability = {
58205
58614
  history: { fields: { on: { kind: "boolean" } } },
58206
58615
  /** Clock fields: written, but excluded from the compare that decides
58207
58616
  * whether persisting is worth a SQLite commit. */
58208
- volatileStateFields: ["lastChangedAt"],
58209
- customize: {
58210
- version: 1,
58211
- advanced: "derive",
58212
- targets: []
58213
- }
58617
+ volatileStateFields: ["lastChangedAt"]
58214
58618
  };
58215
58619
  /**
58216
58620
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -58226,7 +58630,7 @@ var binaryCapability = {
58226
58630
  */
58227
58631
  var BrightnessStatusSchema = object({
58228
58632
  /** Current level as 0..100 inclusive. Firmware-reported. */
58229
- percentage: number().min(0).max(100),
58633
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
58230
58634
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
58231
58635
  lastChangedAt: number()
58232
58636
  });
@@ -58680,21 +59084,21 @@ var ClimateControlStatusSchema = object({
58680
59084
  /** Single setpoint in Celsius. Used by single-target modes
58681
59085
  * (heat / cool / dry). Null when the device is in a range mode
58682
59086
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58683
- target: number().nullable(),
59087
+ target: number().nullable().meta(readingMeta("temperature")),
58684
59088
  /** Upper bound of the dual setpoint in Celsius. Populated only
58685
59089
  * in `heat_cool` mode. */
58686
- targetHigh: number().nullable(),
59090
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58687
59091
  /** Lower bound of the dual setpoint in Celsius. Populated only
58688
59092
  * in `heat_cool` mode. */
58689
- targetLow: number().nullable(),
59093
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58690
59094
  /** Target relative humidity (0..100). Null when the device has no
58691
59095
  * humidity control. */
58692
- targetHumidity: number().min(0).max(100).nullable(),
59096
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58693
59097
  /** Current measured temperature in Celsius. Read-only — pushed by
58694
59098
  * the device's internal sensor. */
58695
- currentTemp: number().nullable(),
59099
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58696
59100
  /** Current measured relative humidity (0..100). Read-only. */
58697
- currentHumidity: number().min(0).max(100).nullable(),
59101
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58698
59102
  /** Vertical louver swing on/off. Null when the device has no vertical
58699
59103
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58700
59104
  swingVertical: boolean().nullable(),
@@ -58702,11 +59106,11 @@ var ClimateControlStatusSchema = object({
58702
59106
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58703
59107
  swingHorizontal: boolean().nullable(),
58704
59108
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58705
- minTemp: number().nullable().optional(),
59109
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58706
59110
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58707
- maxTemp: number().nullable().optional(),
59111
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58708
59112
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58709
- tempStep: number().positive().nullable().optional(),
59113
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58710
59114
  /** Ms epoch when the slice was last updated (push or command). */
58711
59115
  lastFetchedAt: number()
58712
59116
  });
@@ -58848,17 +59252,17 @@ var climateControlCapability = {
58848
59252
  * modes to expose in the picker without a slice fetch.
58849
59253
  */
58850
59254
  var RgbTripletSchema = object({
58851
- r: number().int().min(0).max(255),
58852
- g: number().int().min(0).max(255),
58853
- b: number().int().min(0).max(255)
59255
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59256
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59257
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58854
59258
  });
58855
59259
  var HsvTripletSchema = object({
58856
59260
  /** Hue in degrees, 0..360. */
58857
- h: number().min(0).max(360),
59261
+ h: number().min(0).max(360).meta(readingMeta("none")),
58858
59262
  /** Saturation as 0..100 inclusive. */
58859
- s: number().min(0).max(100),
59263
+ s: number().min(0).max(100).meta(readingMeta("none")),
58860
59264
  /** Value/brightness as 0..100 inclusive. */
58861
- v: number().min(0).max(100)
59265
+ v: number().min(0).max(100).meta(readingMeta("none"))
58862
59266
  });
58863
59267
  var ColorInputSchema = discriminatedUnion("mode", [
58864
59268
  object({
@@ -58887,7 +59291,7 @@ var ColorStatusSchema = object({
58887
59291
  /** Populated when `mode === 'hsv'`. */
58888
59292
  hsv: HsvTripletSchema.optional(),
58889
59293
  /** Populated when `mode === 'mired'`. */
58890
- mireds: number().int().optional(),
59294
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58891
59295
  /** Ms epoch of the last operator-driven change. */
58892
59296
  lastChangedAt: number()
58893
59297
  });
@@ -59082,18 +59486,12 @@ _enum([
59082
59486
  "empty",
59083
59487
  "full"
59084
59488
  ]);
59085
- /** Units a remaining-life counter is reported in. Closed: the panel renders
59086
- * each. */
59087
- var ConsumableRemainingUnitSchema = _enum([
59088
- "days",
59089
- "hours",
59090
- "cycles"
59091
- ]);
59092
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
59093
- * days). `value` null = unknown (D393). */
59489
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59490
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59491
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
59094
59492
  var ConsumableRemainingSchema = object({
59095
- value: number().min(0).nullable(),
59096
- unit: ConsumableRemainingUnitSchema
59493
+ value: number().min(0).nullable().meta(readingMeta("device")),
59494
+ kind: ConsumableRemainingKindSchema
59097
59495
  });
59098
59496
  /** A single consumable item. Either a continuous `level` (remaining
59099
59497
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -59106,7 +59504,7 @@ var ConsumableItemSchema = object({
59106
59504
  /** Display name. */
59107
59505
  label: string().min(1),
59108
59506
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
59109
- level: number().min(0).max(100).nullable(),
59507
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59110
59508
  /** The provider's discrete verdict when known (binary mode). */
59111
59509
  status: ConsumableStatusSchema.nullable(),
59112
59510
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -59188,8 +59586,8 @@ var consumablesCustomize = {
59188
59586
  transforms: [{ kind: "identity" }]
59189
59587
  }],
59190
59588
  literals: [{
59191
- path: "remaining.unit",
59192
- values: [...ConsumableRemainingUnitSchema.options]
59589
+ path: "remaining.kind",
59590
+ values: [...ConsumableRemainingKindSchema.options]
59193
59591
  }]
59194
59592
  },
59195
59593
  {
@@ -59318,7 +59716,6 @@ reset: method(object({
59318
59716
  },
59319
59717
  "remaining.value": {
59320
59718
  kind: "gauge",
59321
- unitFrom: "remaining.unit",
59322
59719
  deadband: 1,
59323
59720
  displayName: "Remaining"
59324
59721
  }
@@ -59388,10 +59785,9 @@ var contactCapability = {
59388
59785
  * (empty array for the other kinds). It lives in the slice because
59389
59786
  * HA `select.*` entities may update their options at runtime.
59390
59787
  *
59391
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59392
- * `kind === 'numeric'` and are absent (undefined) for all other
59393
- * kinds. They are populated live from HA attributes on each state
59394
- * push — consistent with `options` and the sensor-unit approach.
59788
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59789
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59790
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59395
59791
  */
59396
59792
  var ControlStatusSchema = object({
59397
59793
  kind: _enum([
@@ -59403,39 +59799,17 @@ var ControlStatusSchema = object({
59403
59799
  /** Current value. Numeric kind → number; select/text/datetime → string.
59404
59800
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59405
59801
  * or time-only — HA accepts all three forms). */
59406
- value: union([number(), string()]),
59802
+ value: union([number().meta(readingMeta("device")), string()]),
59407
59803
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59408
59804
  options: array(string()),
59409
59805
  /** Ms epoch when the slice was last updated. */
59410
59806
  lastChangedAt: number(),
59411
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59412
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59413
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59414
- unit: string().optional(),
59415
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59416
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59417
- min: number().optional(),
59418
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59419
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59420
- max: number().optional(),
59421
- /** Step increment. Populated live from HA `attributes.step`.
59422
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59423
- step: number().optional(),
59424
- /** Suggested decimal places for numeric display. Populated live from HA
59425
- * `attributes.suggested_display_precision`, or derived from `step` when the
59426
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59427
- * other kinds. Falls back to auto-formatting when absent. */
59428
- precision: number().int().min(0).max(10).optional(),
59429
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59430
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59431
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59432
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59433
- * `has_date`/`has_time` attributes on each push. */
59434
- format: _enum([
59435
- "date",
59436
- "time",
59437
- "datetime"
59438
- ]).optional()
59807
+ /** Minimum allowed value, in the device quantity's base unit. */
59808
+ min: number().optional().meta(readingMeta("device")),
59809
+ /** Maximum allowed value, in the device quantity's base unit. */
59810
+ max: number().optional().meta(readingMeta("device")),
59811
+ /** Step increment, in the device quantity's base unit. */
59812
+ step: number().optional().meta(readingMeta("device"))
59439
59813
  });
59440
59814
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59441
59815
  object({
@@ -59501,9 +59875,9 @@ var CoverStatusSchema = object({
59501
59875
  ]),
59502
59876
  /** 0 = fully closed, 100 = fully open. Null when the device has
59503
59877
  * no intermediate position surface. */
59504
- position: number().min(0).max(100).nullable(),
59878
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59505
59879
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59506
- tiltPosition: number().min(0).max(100).nullable(),
59880
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59507
59881
  /** Ms epoch when the slice was last updated. */
59508
59882
  lastChangedAt: number()
59509
59883
  });
@@ -59606,9 +59980,9 @@ var NormalizedRangeSchema$1 = object({
59606
59980
  var DayNightStatusSchema = object({
59607
59981
  mode: DayNightModeSchema,
59608
59982
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59609
- sensitivity: number().optional(),
59983
+ sensitivity: number().optional().meta(readingMeta("none")),
59610
59984
  /** Delay before the IR-cut filter flips, in seconds. */
59611
- switchDelaySec: number().optional(),
59985
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59612
59986
  lastFetchedAt: number()
59613
59987
  });
59614
59988
  /**
@@ -59826,11 +60200,11 @@ var DishwasherStatusSchema = object({
59826
60200
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59827
60201
  availablePrograms: array(string()),
59828
60202
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59829
- remainingSeconds: number().int().nonnegative().nullable(),
60203
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59830
60204
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59831
- delayedStartSeconds: number().int().nonnegative().nullable(),
60205
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59832
60206
  /** Cycle completion 0..100, or null when not applicable. */
59833
- progressPercent: number().min(0).max(100).nullable(),
60207
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59834
60208
  /** True when the door is reported open; null when the door state is not known. */
59835
60209
  doorOpen: boolean().nullable(),
59836
60210
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59854,7 +60228,7 @@ var DishwasherStatusSchema = object({
59854
60228
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59855
60229
  machineCareDue: boolean().nullable(),
59856
60230
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59857
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
60231
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59858
60232
  /** Option states: null when the appliance does not expose the option or has not said. */
59859
60233
  halfLoad: boolean().nullable(),
59860
60234
  hygienePlus: boolean().nullable(),
@@ -59869,7 +60243,7 @@ var DishwasherStatusSchema = object({
59869
60243
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59870
60244
  activeEvents: array(string()),
59871
60245
  /** Numeric error code when the appliance reports one; null otherwise. */
59872
- errorCode: number().nullable(),
60246
+ errorCode: number().nullable().meta(readingMeta("none")),
59873
60247
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59874
60248
  errorLabel: string().nullable(),
59875
60249
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -60081,6 +60455,7 @@ var doorbellCustomize = {
60081
60455
  applyAuthority: { kind: "composition-claim" },
60082
60456
  eventName: "onPressed",
60083
60457
  edges: ["rising"],
60458
+ defaultEdge: "rising",
60084
60459
  timestampPath: "lastPressedAt",
60085
60460
  counterPath: "pressCountSinceStart",
60086
60461
  highWater: "composer-advance",
@@ -60099,7 +60474,7 @@ var DoorbellStatusSchema = object({
60099
60474
  /** Ms epoch of the last press. null = never observed since this provider started. */
60100
60475
  lastPressedAt: number().nullable(),
60101
60476
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
60102
- pressCountSinceStart: number()
60477
+ pressCountSinceStart: number().meta(readingMeta("count"))
60103
60478
  });
60104
60479
  var DoorbellPressEventSchema = object({
60105
60480
  deviceId: number(),
@@ -60164,17 +60539,7 @@ onPressed: {
60164
60539
  history: "none",
60165
60540
  customize: doorbellCustomize
60166
60541
  };
60167
- /**
60168
- * Enum-state sensor — a string value picked from a finite option set.
60169
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
60170
- * states, weather conditions, contact-source channels, mode strings).
60171
- * The option set is stable across observations; it is persisted in the
60172
- * device config blob (via the control slice at adoption) and is NOT
60173
- * stored in `sourceInfo`.
60174
- */
60175
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
60176
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
60177
- var EnumSensorDateTimeFormatSchema = _enum([
60542
+ _enum([
60178
60543
  "date",
60179
60544
  "time",
60180
60545
  "datetime"
@@ -60182,12 +60547,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
60182
60547
  var EnumSensorStatusSchema = object({
60183
60548
  value: string(),
60184
60549
  /**
60185
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
60186
- * as a locale date/time/datetime; absent for plain enum sensors. HA
60187
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
60188
- * a time-only sensor → `'time'`.
60550
+ * Set when `value` is a timestamp rather than an enum state, so history
60551
+ * does not learn ISO strings as states. The display shape (date / time /
60552
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
60189
60553
  */
60190
- format: EnumSensorDateTimeFormatSchema.optional(),
60554
+ timestamp: boolean().optional(),
60191
60555
  /** Ms epoch when the slice was last updated. */
60192
60556
  lastFetchedAt: number()
60193
60557
  });
@@ -60214,11 +60578,11 @@ var enumSensorCapability = {
60214
60578
  * History (D850): the value as an `enum` series (an appliance's program or
60215
60579
  * operation state, an HVAC action). The option set is per device, so the
60216
60580
  * value list is LEARNED as values are first seen (bounded). A sensor with a
60217
- * `format` carries a timestamp, not a state, and is not recorded.
60581
+ * `timestamp` carries a clock, not a state, and is not recorded.
60218
60582
  */
60219
60583
  history: { fields: { value: {
60220
60584
  kind: "enum",
60221
- skipWhenSet: "format"
60585
+ skipWhenSet: "timestamp"
60222
60586
  } } },
60223
60587
  /** Clock fields: written, but excluded from the compare that decides
60224
60588
  * whether persisting is worth a SQLite commit. */
@@ -60238,13 +60602,13 @@ var EventFireSchema = object({
60238
60602
  deviceId: number(),
60239
60603
  eventType: string(),
60240
60604
  data: record(string(), unknown()).nullable(),
60241
- timestamp: number(),
60242
- seq: number()
60605
+ timestamp: number().meta(readingMeta("none")),
60606
+ seq: number().meta(readingMeta("count"))
60243
60607
  });
60244
60608
  var EventEmitterStatusSchema = object({
60245
60609
  eventTypes: array(string()),
60246
60610
  lastEvent: EventFireSchema.nullable(),
60247
- eventCountSinceStart: number()
60611
+ eventCountSinceStart: number().meta(readingMeta("count"))
60248
60612
  });
60249
60613
  var eventEmitterCapability = {
60250
60614
  name: "event-emitter",
@@ -60657,10 +61021,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60657
61021
  var FanControlStatusSchema = object({
60658
61022
  /** Active speed as 0..100 inclusive. Null when the device has no
60659
61023
  * speed surface (single-speed fan). */
60660
- percentage: number().min(0).max(100).nullable(),
61024
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60661
61025
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60662
61026
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60663
- percentageStep: number().positive().optional(),
61027
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60664
61028
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60665
61029
  * — empty string when no preset is active or supported. */
60666
61030
  preset: string(),
@@ -60781,7 +61145,7 @@ var FeatureProbeStatusSchema = object({
60781
61145
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60782
61146
  model: string().nullable(),
60783
61147
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60784
- channelCount: number().nullable(),
61148
+ channelCount: number().nullable().meta(readingMeta("count")),
60785
61149
  /**
60786
61150
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60787
61151
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60904,9 +61268,91 @@ var gasCapability = {
60904
61268
  * whether persisting is worth a SQLite commit. */
60905
61269
  volatileStateFields: ["lastChangedAt"]
60906
61270
  };
61271
+ var GroupCommandsSchema = object({
61272
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61273
+ results: array(object({
61274
+ deviceId: number().int().nonnegative(),
61275
+ cap: string().min(1),
61276
+ method: string().min(1),
61277
+ ok: boolean(),
61278
+ error: string().optional(),
61279
+ /** Ms epoch of the attempt. */
61280
+ at: number()
61281
+ })) });
61282
+ var groupCommandsCapability = {
61283
+ name: "group-commands",
61284
+ scope: "device",
61285
+ deviceNative: true,
61286
+ mode: "singleton",
61287
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
61288
+ methods: {},
61289
+ status: {
61290
+ schema: GroupCommandsSchema,
61291
+ kind: "push"
61292
+ },
61293
+ runtimeState: GroupCommandsSchema,
61294
+ /**
61295
+ * Runtime-state durability: **session** — the last attempt. A restored
61296
+ * failure would outlive the command that caused it.
61297
+ *
61298
+ * See `RuntimeStateDurability`. Enforced by
61299
+ * `scripts/check-runtime-state-durability.ts`.
61300
+ */
61301
+ durability: "session",
61302
+ history: "none"
61303
+ };
61304
+ /**
61305
+ * How many members of a helper group answered (D944). Counts are integers,
61306
+ * not measurements: zero included is a real count, never a stand-in reading.
61307
+ * The reading itself stays on the output capability. History is none — the
61308
+ * series that later statistics read is the output cap's own history block.
61309
+ */
61310
+ var GroupStatusSchema = object({
61311
+ /** Members whose current value entered the aggregate. */
61312
+ included: number().int().nonnegative().meta(readingMeta("count", {
61313
+ precision: 0,
61314
+ label: "Reporting"
61315
+ })),
61316
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61317
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61318
+ precision: 0,
61319
+ label: "Excluded"
61320
+ })),
61321
+ /** `included` + `excluded`. */
61322
+ total: number().int().nonnegative().meta(readingMeta("count", {
61323
+ precision: 0,
61324
+ label: "Members"
61325
+ }))
61326
+ });
61327
+ var groupStatusCapability = {
61328
+ name: "group-status",
61329
+ scope: "device",
61330
+ deviceNative: true,
61331
+ mode: "singleton",
61332
+ deviceTypes: [
61333
+ DeviceType.Sensor,
61334
+ DeviceType.Switch,
61335
+ DeviceType.Light
61336
+ ],
61337
+ methods: {},
61338
+ status: {
61339
+ schema: GroupStatusSchema,
61340
+ kind: "push"
61341
+ },
61342
+ runtimeState: GroupStatusSchema,
61343
+ /**
61344
+ * Runtime-state durability: **session** — recomputed from members on the
61345
+ * next source event; a restored count would outlive the readings it named.
61346
+ *
61347
+ * See `RuntimeStateDurability`. Enforced by
61348
+ * `scripts/check-runtime-state-durability.ts`.
61349
+ */
61350
+ durability: "session",
61351
+ history: "none"
61352
+ };
60907
61353
  var HeatingValveStatusSchema = object({
60908
61354
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60909
- position: number().min(0).max(100).nullable(),
61355
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60910
61356
  /** Ms epoch when the slice was last updated (push or poll). */
60911
61357
  lastFetchedAt: number()
60912
61358
  });
@@ -60949,9 +61395,9 @@ var HumidifierStatusSchema = object({
60949
61395
  /** Whether the humidifier is currently on. */
60950
61396
  on: boolean(),
60951
61397
  /** Current measured relative humidity (0..100). Null when not reported. */
60952
- currentHumidity: number().min(0).max(100).nullable(),
61398
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60953
61399
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60954
- targetHumidity: number().min(0).max(100).nullable(),
61400
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60955
61401
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60956
61402
  * device has no mode surface. */
60957
61403
  mode: string().nullable(),
@@ -60961,9 +61407,9 @@ var HumidifierStatusSchema = object({
60961
61407
  * `idle` / `off`). Null when the device doesn't report it. */
60962
61408
  action: string().nullable(),
60963
61409
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60964
- minHumidity: number().nullable(),
61410
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60965
61411
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60966
- maxHumidity: number().nullable(),
61412
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60967
61413
  /** Ms epoch when the slice was last updated. */
60968
61414
  lastChangedAt: number()
60969
61415
  });
@@ -61033,22 +61479,13 @@ var humidifierCapability = {
61033
61479
  * `sourceInfo`.
61034
61480
  */
61035
61481
  var HumiditySensorStatusSchema = object({
61036
- /** Current relative humidity, 0..100. */
61037
- percent: number().min(0).max(100).meta({
61038
- unit: "%",
61482
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61483
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61484
+ precision: 0,
61039
61485
  label: "Humidity"
61040
- }),
61486
+ })),
61041
61487
  /** Ms epoch when the slice was last updated. */
61042
- lastFetchedAt: number(),
61043
- /** Live display unit from the upstream source (e.g. HA
61044
- * `attributes.unit_of_measurement`). The UI prefers this over the
61045
- * role's canonical unit. Absent → fall back to the canonical unit. */
61046
- unit: string().optional(),
61047
- /** Suggested decimal places for numeric display.
61048
- * Populated live from the upstream source when provided (e.g. HA
61049
- * `attributes.suggested_display_precision`). Falls back to
61050
- * auto-formatting when absent. */
61051
- precision: number().int().min(0).max(10).optional()
61488
+ lastFetchedAt: number()
61052
61489
  });
61053
61490
  var humiditySensorCapability = {
61054
61491
  name: "humidity-sensor",
@@ -61079,12 +61516,7 @@ var humiditySensorCapability = {
61079
61516
  } } },
61080
61517
  /** Clock fields: written, but excluded from the compare that decides
61081
61518
  * whether persisting is worth a SQLite commit. */
61082
- volatileStateFields: ["lastFetchedAt"],
61083
- customize: {
61084
- version: 1,
61085
- advanced: "derive",
61086
- targets: []
61087
- }
61519
+ volatileStateFields: ["lastFetchedAt"]
61088
61520
  };
61089
61521
  /**
61090
61522
  * Image display cap. Models a single still image exposed by an integration —
@@ -61105,7 +61537,7 @@ var ImageStatusSchema = object({
61105
61537
  * entity exposes no `entity_picture` (yet). */
61106
61538
  url: string().nullable(),
61107
61539
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
61108
- lastUpdated: number().nullable()
61540
+ lastUpdated: number().nullable().meta(readingMeta("none"))
61109
61541
  });
61110
61542
  var imageCapability = {
61111
61543
  name: "image",
@@ -61193,19 +61625,19 @@ var NormalizedRangeSchema = object({
61193
61625
  */
61194
61626
  var ImageSettingsStatusSchema = object({
61195
61627
  /** Normalized 0–100. */
61196
- brightness: number().optional(),
61628
+ brightness: number().optional().meta(readingMeta("none")),
61197
61629
  /** Normalized 0–100. */
61198
- contrast: number().optional(),
61630
+ contrast: number().optional().meta(readingMeta("none")),
61199
61631
  /** Normalized 0–100. */
61200
- saturation: number().optional(),
61632
+ saturation: number().optional().meta(readingMeta("none")),
61201
61633
  /** Normalized 0–100. */
61202
- sharpness: number().optional(),
61634
+ sharpness: number().optional().meta(readingMeta("none")),
61203
61635
  mirror: boolean().optional(),
61204
61636
  flip: boolean().optional(),
61205
61637
  rotate: ImageRotateSchema.optional(),
61206
61638
  whiteBalance: WhiteBalanceModeSchema.optional(),
61207
61639
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
61208
- warmth: number().optional(),
61640
+ warmth: number().optional().meta(readingMeta("none")),
61209
61641
  exposureMode: ExposureModeSchema.optional(),
61210
61642
  backlightMode: BacklightModeSchema.optional(),
61211
61643
  lastFetchedAt: number()
@@ -61484,10 +61916,10 @@ var IntercomAbilitySchema = object({
61484
61916
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61485
61917
  codecs: array(string()),
61486
61918
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61487
- sampleRate: number(),
61919
+ sampleRate: number().meta(readingMeta("none")),
61488
61920
  duplex: _enum(["half", "full"]),
61489
61921
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61490
- maxBacklogMs: number()
61922
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61491
61923
  });
61492
61924
  var IntercomStatusSchema = object({
61493
61925
  /** True while a talk session is open. */
@@ -61714,13 +62146,13 @@ var LawnMowerControlStatusSchema = object({
61714
62146
  activity: LawnMowerActivitySchema,
61715
62147
  /** 0..100 battery percentage. Null when the device has no battery
61716
62148
  * reading. */
61717
- batteryLevel: number().min(0).max(100).nullable(),
62149
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61718
62150
  /** 0..100 mowing-completion percentage of the current task, or null when no
61719
62151
  * task is active / progress is unavailable. */
61720
- progressPercent: number().min(0).max(100).nullable(),
62152
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61721
62153
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61722
62154
  * or null when unknown. */
61723
- currentCode: number().nullable(),
62155
+ currentCode: number().nullable().meta(readingMeta("none")),
61724
62156
  /** Human label for {@link currentCode}, or null when undecodable. */
61725
62157
  currentCodeLabel: string().nullable(),
61726
62158
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62253,7 +62685,7 @@ var MediaPlayerStatusSchema = object({
62253
62685
  state: MediaPlayerStateSchema,
62254
62686
  /** Volume as 0..100 inclusive. Null when the device has no volume
62255
62687
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62256
- volumeLevel: number().min(0).max(100).nullable(),
62688
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62257
62689
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62258
62690
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62259
62691
  isMuted: boolean().nullable(),
@@ -62266,10 +62698,10 @@ var MediaPlayerStatusSchema = object({
62266
62698
  currentMedia: MediaInfoSchema.nullable(),
62267
62699
  /** Current playback position in ms. Null when not seekable or
62268
62700
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62269
- positionMs: number().int().nonnegative().nullable(),
62701
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62270
62702
  /** Total duration of the current media in ms. Null when unknown
62271
62703
  * (live stream) or nothing is playing. */
62272
- durationMs: number().int().nonnegative().nullable(),
62704
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62273
62705
  /** Shuffle toggle. Null when no shuffle surface.
62274
62706
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62275
62707
  shuffle: boolean().nullable(),
@@ -62578,7 +63010,7 @@ var motionCustomize = {
62578
63010
  eventName: "onMotionChanged"
62579
63011
  },
62580
63012
  deviceTypes: [DeviceType.Camera],
62581
- apply: "replace",
63013
+ apply: "add",
62582
63014
  applyAuthority: {
62583
63015
  kind: "camera-sensors",
62584
63016
  timeoutField: "motionCooldownMs"
@@ -62650,7 +63082,7 @@ var MotionStatusSchema = object({
62650
63082
  * it, every provider seeded it at boot, and every session left it
62651
63083
  * `detected: true` after its detach.
62652
63084
  */
62653
- autoClearAfterMs: number().nullable()
63085
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62654
63086
  });
62655
63087
  /**
62656
63088
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62687,12 +63119,18 @@ var motionCapability = {
62687
63119
  methods: {
62688
63120
  /**
62689
63121
  * Pull the current motion state synchronously. Convenience shortcut
62690
- * for consumers that don't need the full status object; equivalent
62691
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62692
- * `getStatus` once the auto-injected status surface lands in every
62693
- * consumer.
63122
+ * for consumers that don't need the full status object. A composed
63123
+ * group answers from its slice (D944): unknown throws, it is not off.
62694
63124
  */
62695
63125
  isDetected: method(object({ deviceId: number() }), boolean()) },
63126
+ /**
63127
+ * A composed motion group serves `isDetected` from `detected` (D944).
63128
+ * Without this the cap stays uncomposable (`has-commands`).
63129
+ */
63130
+ composedMethods: { isDetected: {
63131
+ kind: "slice",
63132
+ field: "detected"
63133
+ } },
62696
63134
  events: {
62697
63135
  /**
62698
63136
  * Fires every time the runner transitions a camera between
@@ -62832,7 +63270,7 @@ var MotionZoneRegionSchema = object({
62832
63270
  * the grid region(s). */
62833
63271
  var MotionZoneStatusSchema = object({
62834
63272
  enabled: boolean(),
62835
- sensitivity: number(),
63273
+ sensitivity: number().meta(readingMeta("none")),
62836
63274
  /** Grid region(s). Today exactly one `grid` shape. */
62837
63275
  regions: array(MotionZoneRegionSchema),
62838
63276
  lastFetchedAt: number()
@@ -63202,7 +63640,7 @@ var NavigationStatusSchema = object({
63202
63640
  following: boolean(),
63203
63641
  flash: boolean(),
63204
63642
  lightMode: NavigationLightModeSchema,
63205
- lightLevel: number().min(40).max(100),
63643
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
63206
63644
  /** Ms epoch when the slice was last updated. */
63207
63645
  lastChangedAt: number()
63208
63646
  });
@@ -63336,13 +63774,13 @@ var NetworkLinkStatusSchema = object({
63336
63774
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63337
63775
  * SKIP a null rather than coerce it.
63338
63776
  */
63339
- signalPercent: number().min(0).max(100).nullable(),
63777
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63340
63778
  /** Raw received signal strength in dBm, when the firmware reports one. */
63341
- rssiDbm: number().optional(),
63779
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63342
63780
  /** Network name of a wireless link, when the firmware reports it. */
63343
63781
  ssid: string().optional(),
63344
63782
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63345
- lastUpdated: number()
63783
+ lastUpdated: number().meta(readingMeta("none"))
63346
63784
  });
63347
63785
  /** The slice a provider seeds before its first read: nothing is known yet. */
63348
63786
  var NETWORK_LINK_UNKNOWN = {
@@ -63747,7 +64185,7 @@ var NotifierStatusSchema = object({
63747
64185
  lastError: string().nullable(),
63748
64186
  /** Number of deliveries currently buffered server-side (rate-limited
63749
64187
  * notifiers may queue). 0 when the provider sends synchronously. */
63750
- queueDepth: number().int().nonnegative(),
64188
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63751
64189
  /** Per-feature capability matrix — authoritative source for the
63752
64190
  * compose-form field gating. */
63753
64191
  supports: NotifierSupportsSchema
@@ -63839,11 +64277,9 @@ onSent: { data: object({
63839
64277
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63840
64278
  * for: water flow, distance, weight, frequency, signal strength, …).
63841
64279
  *
63842
- * The `unit` and `precision` fields carry the live display metadata that
63843
- * the upstream source provides with each state push (HA
63844
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63845
- * The UI reads them directly from the slice — they are the single source
63846
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64280
+ * The quantity is not on this slice. It is chosen when the device is
64281
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64282
+ * that quantity's base unit. `null` is a refused sample (D393).
63847
64283
  *
63848
64284
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63849
64285
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63852,15 +64288,8 @@ onSent: { data: object({
63852
64288
  * numeric values land here.
63853
64289
  */
63854
64290
  var NumericSensorStatusSchema = object({
63855
- value: number(),
63856
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63857
- * Populated live from the upstream source on each state push.
63858
- * Absent for unitless sensors. */
63859
- unit: string().optional(),
63860
- /** Suggested decimal places for numeric display.
63861
- * Populated live from the upstream source when provided.
63862
- * Falls back to auto-formatting when absent. */
63863
- precision: number().int().min(0).max(10).optional(),
64291
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64292
+ value: number().nullable().meta(readingMeta("device")),
63864
64293
  /** Ms epoch when the slice was last updated. */
63865
64294
  lastFetchedAt: number()
63866
64295
  });
@@ -63884,11 +64313,11 @@ var numericSensorCapability = {
63884
64313
  */
63885
64314
  durability: "restored",
63886
64315
  /**
63887
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64316
+ * History (D850): recorded series. The unit is the device quantity's base,
64317
+ * resolved when the series is read — the cap has no single unit.
63888
64318
  */
63889
64319
  history: { fields: { value: {
63890
64320
  kind: "gauge",
63891
- unitFrom: "unit",
63892
64321
  minIntervalMs: 3e4
63893
64322
  } } },
63894
64323
  /** Clock fields: written, but excluded from the compare that decides
@@ -64226,23 +64655,26 @@ var PetFeederStatusSchema = object({
64226
64655
  /** Food currently in the bowl (grams). Null when the device has not
64227
64656
  * reported a reading yet. On dual-hopper models this is the combined
64228
64657
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
64229
- foodLevel: number().nullable(),
64658
+ foodLevel: number().nullable().meta(readingMeta("none")),
64230
64659
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
64231
64660
  * single-hopper models. */
64232
- food1: number().nullable(),
64661
+ food1: number().nullable().meta(readingMeta("none")),
64233
64662
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
64234
64663
  * single-hopper models. */
64235
- food2: number().nullable(),
64664
+ food2: number().nullable().meta(readingMeta("none")),
64236
64665
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
64237
64666
  * (`device_class: problem`, on = low). True when the bowl is empty /
64238
64667
  * below the feeder's low threshold. */
64239
64668
  lowFood: boolean(),
64240
64669
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
64241
64670
  * device has no battery reading. */
64242
- batteryPower: number().min(0).max(100).nullable(),
64243
- /** Days of desiccant life remaining. Null when the model has no
64244
- * desiccant sensor. */
64245
- desiccantLeftDays: number().nullable(),
64671
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64672
+ /**
64673
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64674
+ * Providers convert from the days the feeder reports. Null when the
64675
+ * model has no desiccant sensor.
64676
+ */
64677
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
64246
64678
  /** True while a feed is in progress. */
64247
64679
  feeding: boolean(),
64248
64680
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -64253,7 +64685,7 @@ var PetFeederStatusSchema = object({
64253
64685
  * with `errorCode` for consumers that want the raw integer. */
64254
64686
  error: string().nullable(),
64255
64687
  /** Raw device error code (0 / null = no error). */
64256
- errorCode: number().nullable(),
64688
+ errorCode: number().nullable().meta(readingMeta("none")),
64257
64689
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
64258
64690
  isDualHopper: boolean(),
64259
64691
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -64263,7 +64695,7 @@ var PetFeederStatusSchema = object({
64263
64695
  /** Play a chime when dispensing. */
64264
64696
  feedSound: boolean(),
64265
64697
  /** Speaker / prompt volume level (device-scaled integer). */
64266
- volume: number(),
64698
+ volume: number().meta(readingMeta("none")),
64267
64699
  /** Ms epoch when the slice was last refreshed from the cloud. */
64268
64700
  lastFetchedAt: number()
64269
64701
  });
@@ -64382,7 +64814,7 @@ var petFeederCapability = {
64382
64814
  },
64383
64815
  desiccantLeftDays: {
64384
64816
  kind: "gauge",
64385
- unit: "days"
64817
+ unit: "s"
64386
64818
  }
64387
64819
  } }
64388
64820
  };
@@ -64783,36 +65215,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64783
65215
  */
64784
65216
  var PowerMeterStatusSchema = object({
64785
65217
  /** Instantaneous power draw in watts. */
64786
- watts: number().optional().meta({
64787
- unit: "W",
64788
- label: "Power"
64789
- }),
65218
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64790
65219
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64791
- kwhTotal: number().optional().meta({
64792
- unit: "kWh",
64793
- label: "Energy"
64794
- }),
65220
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64795
65221
  /** Voltage in volts. */
64796
- volts: number().optional(),
65222
+ volts: number().optional().meta(readingMeta("voltage")),
64797
65223
  /** Current in amperes. */
64798
- amps: number().optional(),
64799
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64800
- va: number().optional(),
65224
+ amps: number().optional().meta(readingMeta("current")),
65225
+ /** Apparent power in volt-amperes. */
65226
+ va: number().optional().meta(readingMeta("apparent-power")),
64801
65227
  /** Power factor (0..1) if reported. */
64802
- powerFactor: number().min(0).max(1).optional(),
65228
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64803
65229
  /** Ms epoch when the slice was last updated. */
64804
- lastFetchedAt: number(),
64805
- /** Live display unit of the single metric this slice carries (e.g. HA
64806
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64807
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64808
- * unit per slice is unambiguous. The UI prefers this over the role's
64809
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64810
- unit: string().optional(),
64811
- /** Suggested decimal places for numeric display.
64812
- * Populated live from the upstream source when provided (e.g. HA
64813
- * `attributes.suggested_display_precision`). Falls back to
64814
- * auto-formatting when absent. */
64815
- precision: number().int().min(0).max(10).optional()
65230
+ lastFetchedAt: number()
64816
65231
  });
64817
65232
  var powerMeterCapability = {
64818
65233
  name: "power-meter",
@@ -64876,12 +65291,7 @@ var powerMeterCapability = {
64876
65291
  } },
64877
65292
  /** Clock fields: written, but excluded from the compare that decides
64878
65293
  * whether persisting is worth a SQLite commit. */
64879
- volatileStateFields: ["lastFetchedAt"],
64880
- customize: {
64881
- version: 1,
64882
- advanced: "derive",
64883
- targets: []
64884
- }
65294
+ volatileStateFields: ["lastFetchedAt"]
64885
65295
  };
64886
65296
  /**
64887
65297
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64901,11 +65311,11 @@ var powerMeterCapability = {
64901
65311
  */
64902
65312
  var GpsLocationSchema = object({
64903
65313
  /** Latitude in decimal degrees, -90..90. */
64904
- latitude: number().min(-90).max(90),
65314
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64905
65315
  /** Longitude in decimal degrees, -180..180. */
64906
- longitude: number().min(-180).max(180),
65316
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64907
65317
  /** Reported accuracy in meters (lower = better). */
64908
- accuracyMeters: number().nonnegative()
65318
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64909
65319
  });
64910
65320
  var PresenceStatusSchema = object({
64911
65321
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64917,7 +65327,7 @@ var PresenceStatusSchema = object({
64917
65327
  gps: GpsLocationSchema.nullable(),
64918
65328
  /** Optional battery level of the tracking device (0..100). Null
64919
65329
  * when not reported. */
64920
- batteryPercent: number().min(0).max(100).nullable(),
65330
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64921
65331
  /** Ms epoch when the slice was last updated. */
64922
65332
  lastChangedAt: number()
64923
65333
  });
@@ -64967,19 +65377,10 @@ var presenceCapability = {
64967
65377
  * `sourceInfo`.
64968
65378
  */
64969
65379
  var PressureSensorStatusSchema = object({
64970
- /** Current pressure in hPa. */
64971
- hpa: number(),
65380
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65381
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64972
65382
  /** Ms epoch when the slice was last updated. */
64973
- lastFetchedAt: number(),
64974
- /** Live display unit from the upstream source (e.g. HA
64975
- * `attributes.unit_of_measurement`). The UI prefers this over the
64976
- * role's canonical unit. Absent → fall back to the canonical unit. */
64977
- unit: string().optional(),
64978
- /** Suggested decimal places for numeric display.
64979
- * Populated live from the upstream source when provided (e.g. HA
64980
- * `attributes.suggested_display_precision`). Falls back to
64981
- * auto-formatting when absent. */
64982
- precision: number().int().min(0).max(10).optional()
65383
+ lastFetchedAt: number()
64983
65384
  });
64984
65385
  var pressureSensorCapability = {
64985
65386
  name: "pressure-sensor",
@@ -65505,8 +65906,8 @@ var ptzCapability = {
65505
65906
  };
65506
65907
  var PtzAutotrackSettingsSchema = object({
65507
65908
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65508
- stopDelaySeconds: number().int().min(0).max(300),
65509
- disappearDelaySeconds: number().int().min(0).max(300)
65909
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65910
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65510
65911
  });
65511
65912
  /**
65512
65913
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65660,7 +66061,7 @@ var RunningStateStatusSchema = object({
65660
66061
  /** null = unknown — never coerced to false (D393). */
65661
66062
  running: boolean().nullable(),
65662
66063
  /** Epoch ms the current boolean began; null while `running` is null. */
65663
- since: number().nullable(),
66064
+ since: number().nullable().meta(readingMeta("none")),
65664
66065
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65665
66066
  phase: string().nullable(),
65666
66067
  /** null until a reading has been published (D393). */
@@ -65675,7 +66076,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65675
66076
  /** Generic Customize target for the running boolean (D936). */
65676
66077
  var runningCustomize = {
65677
66078
  version: 1,
65678
- advanced: "derive",
65679
66079
  targets: [{
65680
66080
  id: "running",
65681
66081
  cardinality: "scalar",
@@ -65695,7 +66095,7 @@ var runningCustomize = {
65695
66095
  DeviceType.Thermostat,
65696
66096
  DeviceType.PetFeeder
65697
66097
  ],
65698
- apply: "add-or-replace",
66098
+ apply: "add",
65699
66099
  path: "running",
65700
66100
  sources: [
65701
66101
  {
@@ -65711,6 +66111,7 @@ var runningCustomize = {
65711
66111
  requiredUnit: "W",
65712
66112
  transforms: [{
65713
66113
  kind: "threshold",
66114
+ comparisons: ["gte", "lte"],
65714
66115
  comparison: "gte",
65715
66116
  defaultThreshold: 5,
65716
66117
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -66987,9 +67388,9 @@ var RecordTriggerSchema = _enum([
66987
67388
  */
66988
67389
  var RecordWindowSchema = object({
66989
67390
  trigger: RecordTriggerSchema,
66990
- day: number().int().min(0).max(6),
66991
- startMinute: number().int().min(0).max(1439),
66992
- endMinute: number().int().min(1).max(1440)
67391
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67392
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67393
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66993
67394
  });
66994
67395
  /** Status of one physical volume, as the camera itself describes it. */
66995
67396
  var OnboardStorageVolumeSchema = object({
@@ -67065,10 +67466,10 @@ var RecordingOnboardStatusSchema = object({
67065
67466
  /** Master "record to the card at all" switch. */
67066
67467
  enabled: boolean().nullable(),
67067
67468
  overwriteWhenFull: boolean().nullable(),
67068
- preRecordSec: number().nullable(),
67069
- postRecordSec: number().nullable(),
67469
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67470
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
67070
67471
  /** Length of one recorded file, in minutes. */
67071
- segmentMinutes: number().nullable(),
67472
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
67072
67473
  /** The primary track's weekly windows, flattened. */
67073
67474
  windows: array(RecordWindowSchema),
67074
67475
  /**
@@ -67084,7 +67485,7 @@ var RecordingOnboardStatusSchema = object({
67084
67485
  * provider reporting a non-zero count also reports the schedule as not
67085
67486
  * writable.
67086
67487
  */
67087
- unreadableWindows: number(),
67488
+ unreadableWindows: number().meta(readingMeta("count")),
67088
67489
  /**
67089
67490
  * The camera is scheduled to record and has NO usable storage.
67090
67491
  *
@@ -68094,7 +68495,7 @@ var CamStreamDescriptorSchema = object({
68094
68495
  url: string().optional(),
68095
68496
  codec: string().optional(),
68096
68497
  resolution: CamStreamResolutionSchema.optional(),
68097
- fps: number().positive().optional(),
68498
+ fps: number().positive().optional().meta(readingMeta("rate")),
68098
68499
  label: string().optional(),
68099
68500
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
68100
68501
  deviceFeatures: array(string()).optional(),
@@ -68181,18 +68582,18 @@ var StreamProfileSchema = _enum([
68181
68582
  ]);
68182
68583
  /** The current encoding config of one stream profile. */
68183
68584
  var StreamProfileConfigSchema = object({
68184
- width: number(),
68185
- height: number(),
68585
+ width: number().meta(readingMeta("none")),
68586
+ height: number().meta(readingMeta("none")),
68186
68587
  codec: _enum(["h264", "h265"]),
68187
- framerate: number(),
68188
- bitrate: number(),
68588
+ framerate: number().meta(readingMeta("rate")),
68589
+ bitrate: number().meta(readingMeta("bitrate")),
68189
68590
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
68190
68591
  encoderProfile: _enum([
68191
68592
  "high",
68192
68593
  "main",
68193
68594
  "baseline"
68194
68595
  ]).optional(),
68195
- gop: number().optional(),
68596
+ gop: number().optional().meta(readingMeta("none")),
68196
68597
  /**
68197
68598
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
68198
68599
  *
@@ -68611,12 +69012,7 @@ var switchCapability = {
68611
69012
  label: "On",
68612
69013
  kind: "boolean"
68613
69014
  }
68614
- }] },
68615
- customize: {
68616
- version: 1,
68617
- advanced: "derive",
68618
- targets: []
68619
- }
69015
+ }] }
68620
69016
  };
68621
69017
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
68622
69018
  function hasControlCharacter(value) {
@@ -69199,35 +69595,21 @@ var tamperCapability = {
69199
69595
  * entries with `device_class: temperature` and any future native
69200
69596
  * thermometer.
69201
69597
  *
69202
- * Unit normalisation: providers convert to Celsius before storing.
69203
- * The slice value is always Celsius so cross-cap aggregators
69204
- * (climate-control's `currentTemp`, energy analytics) can compose
69205
- * without per-source unit fixups. The canonical display unit (`°C`) is
69206
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
69207
- * `sourceInfo`.
69598
+ * The number is Celsius (D944). Providers convert on ingest. A failed
69599
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
69208
69600
  *
69209
69601
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
69210
69602
  * frozen feed (provider hung) distinct from a "temperature hasn't
69211
69603
  * changed" steady state.
69212
69604
  */
69213
69605
  var TemperatureSensorStatusSchema = object({
69214
- /** Current temperature in Celsius. */
69215
- celsius: number().meta({
69216
- unit: "°C",
69606
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
69607
+ celsius: number().nullable().meta(readingMeta("temperature", {
69608
+ precision: 1,
69217
69609
  label: "Temperature"
69218
- }),
69610
+ })),
69219
69611
  /** Ms epoch when the slice was last updated (push or poll). */
69220
- lastFetchedAt: number(),
69221
- /** Live display unit from the upstream source (e.g. HA
69222
- * `attributes.unit_of_measurement`). The UI prefers this over the
69223
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
69224
- * Absent → the UI falls back to the role's canonical unit. */
69225
- unit: string().optional(),
69226
- /** Suggested decimal places for numeric display.
69227
- * Populated live from the upstream source when provided (e.g. HA
69228
- * `attributes.suggested_display_precision`). Falls back to
69229
- * auto-formatting when absent. */
69230
- precision: number().int().min(0).max(10).optional()
69612
+ lastFetchedAt: number()
69231
69613
  });
69232
69614
  var temperatureSensorCapability = {
69233
69615
  name: "temperature-sensor",
@@ -69258,12 +69640,7 @@ var temperatureSensorCapability = {
69258
69640
  } } },
69259
69641
  /** Clock fields: written, but excluded from the compare that decides
69260
69642
  * whether persisting is worth a SQLite commit. */
69261
- volatileStateFields: ["lastFetchedAt"],
69262
- customize: {
69263
- version: 1,
69264
- advanced: "derive",
69265
- targets: []
69266
- }
69643
+ volatileStateFields: ["lastFetchedAt"]
69267
69644
  };
69268
69645
  /**
69269
69646
  * toast — system-scoped singleton capability that streams toast
@@ -69546,16 +69923,16 @@ var VacuumControlStatusSchema = object({
69546
69923
  ]),
69547
69924
  /** 0..100 battery percentage. Null when the device has no battery
69548
69925
  * reading. */
69549
- batteryLevel: number().min(0).max(100).nullable(),
69926
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69550
69927
  /** Current fan-speed token (provider-verbatim). Null when unknown or
69551
69928
  * the vacuum has no speed control. */
69552
69929
  fanSpeed: string().nullable(),
69553
69930
  /** Speed tokens the hardware accepts — drives the UI selector. */
69554
69931
  availableFanSpeeds: array(string()),
69555
69932
  /** 0..100 cleaning-completion percentage of the current task, or null. */
69556
- progressPercent: number().min(0).max(100).nullable(),
69933
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69557
69934
  /** Current error code (0 / null = no error). */
69558
- errorCode: number().nullable(),
69935
+ errorCode: number().nullable().meta(readingMeta("none")),
69559
69936
  /** Human label for {@link errorCode}, or null when none / undecodable. */
69560
69937
  errorLabel: string().nullable(),
69561
69938
  /** Ms epoch when the slice was last updated. */
@@ -69633,7 +70010,7 @@ var ValveStatusSchema = object({
69633
70010
  ]),
69634
70011
  /** 0 = fully closed, 100 = fully open. Null when the device has no
69635
70012
  * intermediate position surface. */
69636
- position: number().min(0).max(100).nullable(),
70013
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69637
70014
  /** Ms epoch when the slice was last updated. */
69638
70015
  lastChangedAt: number()
69639
70016
  });
@@ -69741,9 +70118,9 @@ var vibrationCapability = {
69741
70118
  */
69742
70119
  var WaterHeaterStatusSchema = object({
69743
70120
  /** Current measured temperature. Null when not reported. */
69744
- currentTemp: number().nullable(),
70121
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69745
70122
  /** Target temperature setpoint. Null when no setpoint surface. */
69746
- targetTemp: number().nullable(),
70123
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69747
70124
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69748
70125
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69749
70126
  * device reports an unknown state. */
@@ -69754,9 +70131,9 @@ var WaterHeaterStatusSchema = object({
69754
70131
  * has no away surface. */
69755
70132
  away: boolean().nullable(),
69756
70133
  /** HA `min_temp` attribute. Null when not reported. */
69757
- minTemp: number().nullable(),
70134
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69758
70135
  /** HA `max_temp` attribute. Null when not reported. */
69759
- maxTemp: number().nullable(),
70136
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69760
70137
  /** Ms epoch when the slice was last updated. */
69761
70138
  lastChangedAt: number()
69762
70139
  });
@@ -69841,25 +70218,18 @@ var waterHeaterCapability = {
69841
70218
  * Forecast deferred — current conditions only for v1.
69842
70219
  */
69843
70220
  var WeatherStatusSchema = object({
69844
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69845
- * when no condition has been reported yet. */
70221
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69846
70222
  condition: string().nullable(),
69847
- /** Current temperature in the reported unit. Null when not provided. */
69848
- temperature: number().nullable(),
69849
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69850
- temperatureUnit: string().nullable(),
70223
+ /** Current temperature in Celsius. Null when not provided or refused. */
70224
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69851
70225
  /** Relative humidity (0..100). Null when not provided. */
69852
- humidity: number().min(0).max(100).nullable(),
69853
- /** Barometric pressure in the reported unit. Null when not provided. */
69854
- pressure: number().nullable(),
69855
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69856
- pressureUnit: string().nullable(),
69857
- /** Wind speed in the reported unit. Null when not provided. */
69858
- windSpeed: number().nullable(),
69859
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69860
- windSpeedUnit: string().nullable(),
70226
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
70227
+ /** Barometric pressure in hPa. Null when not provided or refused. */
70228
+ pressure: number().nullable().meta(readingMeta("pressure")),
70229
+ /** Wind speed in metres per second. Null when not provided or refused. */
70230
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69861
70231
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69862
- windBearing: number().nullable(),
70232
+ windBearing: number().nullable().meta(readingMeta("angle")),
69863
70233
  /** Ms epoch when the slice was last updated. */
69864
70234
  lastFetchedAt: number()
69865
70235
  });
@@ -69887,12 +70257,12 @@ var weatherCapability = {
69887
70257
  */
69888
70258
  durability: "session",
69889
70259
  /**
69890
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
70260
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69891
70261
  */
69892
70262
  history: { fields: {
69893
70263
  temperature: {
69894
70264
  kind: "gauge",
69895
- unitFrom: "temperatureUnit",
70265
+ unit: "°C",
69896
70266
  deadband: .2
69897
70267
  },
69898
70268
  humidity: {
@@ -69902,12 +70272,12 @@ var weatherCapability = {
69902
70272
  },
69903
70273
  pressure: {
69904
70274
  kind: "gauge",
69905
- unitFrom: "pressureUnit",
70275
+ unit: "hPa",
69906
70276
  deadband: .5
69907
70277
  },
69908
70278
  windSpeed: {
69909
70279
  kind: "gauge",
69910
- unitFrom: "windSpeedUnit",
70280
+ unit: "m/s",
69911
70281
  deadband: .5
69912
70282
  }
69913
70283
  } }
@@ -71286,6 +71656,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
71286
71656
  featureProbe: featureProbeCapability,
71287
71657
  flood: floodCapability,
71288
71658
  gas: gasCapability,
71659
+ groupCommands: groupCommandsCapability,
71660
+ groupStatus: groupStatusCapability,
71289
71661
  heatingValve: heatingValveCapability,
71290
71662
  humidifier: humidifierCapability,
71291
71663
  humiditySensor: humiditySensorCapability,
@@ -72662,6 +73034,13 @@ Object.freeze({
72662
73034
  access: "view",
72663
73035
  role: "admin"
72664
73036
  },
73037
+ "addons.getPackageDocs": {
73038
+ capName: "addons",
73039
+ capScope: "system",
73040
+ addonId: null,
73041
+ access: "view",
73042
+ role: "admin"
73043
+ },
72665
73044
  "addons.getVersions": {
72666
73045
  capName: "addons",
72667
73046
  capScope: "system",
@@ -75770,6 +76149,20 @@ Object.freeze({
75770
76149
  access: "view",
75771
76150
  role: "viewer"
75772
76151
  },
76152
+ "groupCommands.getStatus": {
76153
+ capName: "group-commands",
76154
+ capScope: "device",
76155
+ addonId: null,
76156
+ access: "view",
76157
+ role: "viewer"
76158
+ },
76159
+ "groupStatus.getStatus": {
76160
+ capName: "group-status",
76161
+ capScope: "device",
76162
+ addonId: null,
76163
+ access: "view",
76164
+ role: "viewer"
76165
+ },
75773
76166
  "heatingValve.getStatus": {
75774
76167
  capName: "heating-valve",
75775
76168
  capScope: "device",
@@ -85592,6 +85985,37 @@ var SENSOR_CAP_VOCABULARY = {
85592
85985
  icon: "flame"
85593
85986
  }]
85594
85987
  },
85988
+ "group-commands": {
85989
+ scope: "device",
85990
+ noun: t("Group command", "Comando di gruppo")
85991
+ },
85992
+ "group-status": {
85993
+ scope: "device",
85994
+ noun: t("Group", "Gruppo"),
85995
+ state: [
85996
+ {
85997
+ kind: "numeric",
85998
+ field: "included",
85999
+ quantity: t("the members reporting", "i membri che rispondono"),
86000
+ label: t("Reporting", "Rispondono"),
86001
+ icon: "users"
86002
+ },
86003
+ {
86004
+ kind: "numeric",
86005
+ field: "excluded",
86006
+ quantity: t("the members left out", "i membri esclusi"),
86007
+ label: t("Excluded", "Esclusi"),
86008
+ icon: "users"
86009
+ },
86010
+ {
86011
+ kind: "numeric",
86012
+ field: "total",
86013
+ quantity: t("the member count", "il numero di membri"),
86014
+ label: t("Members", "Membri"),
86015
+ icon: "sigma"
86016
+ }
86017
+ ]
86018
+ },
85595
86019
  "humidity-sensor": {
85596
86020
  scope: "device",
85597
86021
  noun: t("Humidity", "Umidità"),
@@ -85625,7 +86049,6 @@ var SENSOR_CAP_VOCABULARY = {
85625
86049
  field: "value",
85626
86050
  quantity: t("the reading", "il valore"),
85627
86051
  label: t("Reading", "Valore"),
85628
- unitField: "unit",
85629
86052
  icon: "gauge"
85630
86053
  }]
85631
86054
  },
@@ -85761,7 +86184,7 @@ var SENSOR_CAP_VOCABULARY = {
85761
86184
  field: "temperature",
85762
86185
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85763
86186
  label: t("Temperature", "Temperatura"),
85764
- unitField: "temperatureUnit",
86187
+ unit: "°C",
85765
86188
  icon: "thermometer"
85766
86189
  },
85767
86190
  {
@@ -85777,7 +86200,7 @@ var SENSOR_CAP_VOCABULARY = {
85777
86200
  field: "windSpeed",
85778
86201
  quantity: t("the wind speed", "la velocità del vento"),
85779
86202
  label: t("Wind", "Vento"),
85780
- unitField: "windSpeedUnit",
86203
+ unit: "m/s",
85781
86204
  icon: "wind"
85782
86205
  },
85783
86206
  {
@@ -85785,7 +86208,7 @@ var SENSOR_CAP_VOCABULARY = {
85785
86208
  field: "pressure",
85786
86209
  quantity: t("the air pressure", "la pressione atmosferica"),
85787
86210
  label: t("Pressure", "Pressione"),
85788
- unitField: "pressureUnit",
86211
+ unit: "hPa",
85789
86212
  icon: "gauge"
85790
86213
  }
85791
86214
  ]
@@ -87210,6 +87633,16 @@ Object.freeze({
87210
87633
  form: "single",
87211
87634
  optional: false
87212
87635
  }],
87636
+ "groupCommands.getStatus": [{
87637
+ name: "deviceId",
87638
+ form: "single",
87639
+ optional: false
87640
+ }],
87641
+ "groupStatus.getStatus": [{
87642
+ name: "deviceId",
87643
+ form: "single",
87644
+ optional: false
87645
+ }],
87213
87646
  "heatingValve.getStatus": [{
87214
87647
  name: "deviceId",
87215
87648
  form: "single",
@@ -89074,6 +89507,11 @@ var PulseOverrideDraftSchema = object({
89074
89507
  targetId: string().min(1),
89075
89508
  enabled: boolean(),
89076
89509
  debounceMs: number().nonnegative(),
89510
+ edges: array(_enum([
89511
+ "rising",
89512
+ "button",
89513
+ "native-event"
89514
+ ])).min(1).optional(),
89077
89515
  sources: array(OverrideSourceSchema).min(1)
89078
89516
  });
89079
89517
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -89189,6 +89627,8 @@ new Map(Object.entries({
89189
89627
  "detection-blind": "device-events",
89190
89628
  "motion-noisy": "device-events",
89191
89629
  "motion-calm": "device-events",
89630
+ "package-llm-silent": "device-events",
89631
+ "package-llm-answered": "device-events",
89192
89632
  "recording-stalled": "device-events",
89193
89633
  "recording-healthy": "device-events",
89194
89634
  "recording-degraded": "device-events",
@@ -89272,6 +89712,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
89272
89712
  label: "Motion without a subject",
89273
89713
  kinds: ["motion-noisy", "motion-calm"]
89274
89714
  },
89715
+ {
89716
+ id: "package-llm",
89717
+ family: "device-events",
89718
+ label: "Package confirm",
89719
+ kinds: ["package-llm-silent", "package-llm-answered"]
89720
+ },
89275
89721
  {
89276
89722
  id: "recording",
89277
89723
  family: "device-events",
@@ -89453,6 +89899,8 @@ var DEVICE_EVENTS = [
89453
89899
  "detection-blind",
89454
89900
  "motion-noisy",
89455
89901
  "motion-calm",
89902
+ "package-llm-silent",
89903
+ "package-llm-answered",
89456
89904
  "recording-stalled",
89457
89905
  "recording-healthy",
89458
89906
  "recording-degraded",
@@ -89721,6 +90169,15 @@ new Map([
89721
90169
  snapshotLabel: "Cameras with constant motion now",
89722
90170
  snapshotSection: null
89723
90171
  },
90172
+ {
90173
+ id: "package-llm-confirm",
90174
+ family: "device-events",
90175
+ label: "Package confirm (not answering / answering again)",
90176
+ bad: ["package-llm-silent"],
90177
+ recovery: ["package-llm-answered"],
90178
+ snapshotLabel: "Cameras whose package confirm is not answering",
90179
+ snapshotSection: null
90180
+ },
89724
90181
  {
89725
90182
  id: "recording-health",
89726
90183
  family: "device-events",