@camstack/addon-provider-rtsp 1.2.146 → 1.2.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import net from "node:net";
2
- //#region ../types/dist/event-category-DHXxB8t9.mjs
2
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -325,6 +325,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
325
325
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
326
326
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
327
327
  /**
328
+ * A camera's package LLM confirm has failed closed three times in a row
329
+ * (D931, 2026-10-26). One edge per camera until
330
+ * {@link PipelinePackageLlmAnswered}.
331
+ */
332
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
333
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
334
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
335
+ /**
328
336
  * Per-camera pipeline config was mutated by the orchestrator
329
337
  * (a settings change via `setAgentInferenceDevices` /
330
338
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5434,7 +5442,7 @@ var ZodIssueCode = {
5434
5442
  var ZodFirstPartyTypeKind;
5435
5443
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5436
5444
  //#endregion
5437
- //#region ../types/dist/sleep-BErAxhul.mjs
5445
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5438
5446
  /**
5439
5447
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5440
5448
  * window to float samples (D455).
@@ -7988,6 +7996,99 @@ var RecordKindDeclarationSchema = object({
7988
7996
  }).strict();
7989
7997
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7990
7998
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7999
+ var READING_QUANTITIES = [
8000
+ "temperature",
8001
+ "pressure",
8002
+ "speed",
8003
+ "precipitation",
8004
+ "precipitation-rate",
8005
+ "length",
8006
+ "area",
8007
+ "illuminance",
8008
+ "irradiance",
8009
+ "power",
8010
+ "energy",
8011
+ "voltage",
8012
+ "current",
8013
+ "apparent-power",
8014
+ "humidity",
8015
+ "percentage",
8016
+ "concentration-volume",
8017
+ "concentration-mass",
8018
+ "concentration-ppb",
8019
+ "volume",
8020
+ "duration",
8021
+ "duration-ms",
8022
+ "information",
8023
+ "sound-level",
8024
+ "signal-level",
8025
+ "colour-temperature",
8026
+ "angle",
8027
+ "bitrate",
8028
+ "rate",
8029
+ "mass",
8030
+ "index",
8031
+ "ratio",
8032
+ "count",
8033
+ "none",
8034
+ "device"
8035
+ ];
8036
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8037
+ new Set(READING_QUANTITIES);
8038
+ /**
8039
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8040
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8041
+ */
8042
+ var QUANTITY_BASE = {
8043
+ temperature: "°C",
8044
+ pressure: "hPa",
8045
+ speed: "m/s",
8046
+ precipitation: "mm",
8047
+ "precipitation-rate": "mm/h",
8048
+ length: "m",
8049
+ area: "m²",
8050
+ illuminance: "lx",
8051
+ irradiance: "W/m²",
8052
+ power: "W",
8053
+ energy: "kWh",
8054
+ voltage: "V",
8055
+ current: "A",
8056
+ "apparent-power": "VA",
8057
+ humidity: "%",
8058
+ percentage: "%",
8059
+ "concentration-volume": "ppm",
8060
+ "concentration-mass": "µg/m³",
8061
+ "concentration-ppb": "ppb",
8062
+ volume: "L",
8063
+ duration: "s",
8064
+ "duration-ms": "ms",
8065
+ information: "B",
8066
+ "sound-level": "dBFS",
8067
+ "signal-level": "dBm",
8068
+ "colour-temperature": "mired",
8069
+ angle: "°",
8070
+ bitrate: "kbps",
8071
+ rate: "fps",
8072
+ mass: "kg",
8073
+ index: "none",
8074
+ ratio: "none",
8075
+ count: "none",
8076
+ none: "none",
8077
+ device: "device"
8078
+ };
8079
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8080
+ function baseUnitOf(quantity) {
8081
+ const symbol = QUANTITY_BASE[quantity];
8082
+ if (symbol === "none" || symbol === "device") return void 0;
8083
+ return symbol;
8084
+ }
8085
+ function readingMeta(quantity, extras) {
8086
+ return {
8087
+ quantity,
8088
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8089
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8090
+ };
8091
+ }
7991
8092
  /** Shared Zod schemas used across streaming capabilities. */
7992
8093
  var CamProfileSchema = _enum([
7993
8094
  "high",
@@ -8015,8 +8116,8 @@ var CamStreamKindSchema = _enum([
8015
8116
  "derived"
8016
8117
  ]);
8017
8118
  var CamStreamResolutionSchema = object({
8018
- width: number().int().positive(),
8019
- height: number().int().positive()
8119
+ width: number().int().positive().meta(readingMeta("none")),
8120
+ height: number().int().positive().meta(readingMeta("none"))
8020
8121
  });
8021
8122
  var CameraStreamSchema = object({
8022
8123
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8534,6 +8635,8 @@ var CAP_NAMES = Object.freeze([
8534
8635
  "filesystem-browse",
8535
8636
  "flood",
8536
8637
  "gas",
8638
+ "group-commands",
8639
+ "group-status",
8537
8640
  "heating-valve",
8538
8641
  "humidifier",
8539
8642
  "humidity-sensor",
@@ -8663,14 +8766,6 @@ Object.freeze({
8663
8766
  {
8664
8767
  "path": "aqi",
8665
8768
  "valueType": "number"
8666
- },
8667
- {
8668
- "path": "unit",
8669
- "valueType": "string"
8670
- },
8671
- {
8672
- "path": "precision",
8673
- "valueType": "number"
8674
8769
  }
8675
8770
  ],
8676
8771
  "alarm-panel": [
@@ -8706,20 +8801,10 @@ Object.freeze({
8706
8801
  "valueType": "boolean"
8707
8802
  }
8708
8803
  ],
8709
- "ambient-light-sensor": [
8710
- {
8711
- "path": "lux",
8712
- "valueType": "number"
8713
- },
8714
- {
8715
- "path": "unit",
8716
- "valueType": "string"
8717
- },
8718
- {
8719
- "path": "precision",
8720
- "valueType": "number"
8721
- }
8722
- ],
8804
+ "ambient-light-sensor": [{
8805
+ "path": "lux",
8806
+ "valueType": "number"
8807
+ }],
8723
8808
  "audio-metrics": [
8724
8809
  {
8725
8810
  "path": "ts",
@@ -8799,6 +8884,10 @@ Object.freeze({
8799
8884
  "path": "lastUpdated",
8800
8885
  "valueType": "number"
8801
8886
  },
8887
+ {
8888
+ "path": "volts",
8889
+ "valueType": "number"
8890
+ },
8802
8891
  {
8803
8892
  "path": "lastContactAt",
8804
8893
  "valueType": "number"
@@ -9041,10 +9130,6 @@ Object.freeze({
9041
9130
  "datetime"
9042
9131
  ]
9043
9132
  },
9044
- {
9045
- "path": "unit",
9046
- "valueType": "string"
9047
- },
9048
9133
  {
9049
9134
  "path": "min",
9050
9135
  "valueType": "number"
@@ -9056,19 +9141,6 @@ Object.freeze({
9056
9141
  {
9057
9142
  "path": "step",
9058
9143
  "valueType": "number"
9059
- },
9060
- {
9061
- "path": "precision",
9062
- "valueType": "number"
9063
- },
9064
- {
9065
- "path": "format",
9066
- "valueType": "enum",
9067
- "enumValues": [
9068
- "date",
9069
- "time",
9070
- "datetime"
9071
- ]
9072
9144
  }
9073
9145
  ],
9074
9146
  "cover": [
@@ -9309,13 +9381,8 @@ Object.freeze({
9309
9381
  "path": "value",
9310
9382
  "valueType": "string"
9311
9383
  }, {
9312
- "path": "format",
9313
- "valueType": "enum",
9314
- "enumValues": [
9315
- "date",
9316
- "time",
9317
- "datetime"
9318
- ]
9384
+ "path": "timestamp",
9385
+ "valueType": "boolean"
9319
9386
  }],
9320
9387
  "event-emitter": [
9321
9388
  {
@@ -9388,6 +9455,20 @@ Object.freeze({
9388
9455
  "path": "detected",
9389
9456
  "valueType": "boolean"
9390
9457
  }],
9458
+ "group-status": [
9459
+ {
9460
+ "path": "included",
9461
+ "valueType": "number"
9462
+ },
9463
+ {
9464
+ "path": "excluded",
9465
+ "valueType": "number"
9466
+ },
9467
+ {
9468
+ "path": "total",
9469
+ "valueType": "number"
9470
+ }
9471
+ ],
9391
9472
  "heating-valve": [{
9392
9473
  "path": "position",
9393
9474
  "valueType": "number"
@@ -9422,20 +9503,10 @@ Object.freeze({
9422
9503
  "valueType": "number"
9423
9504
  }
9424
9505
  ],
9425
- "humidity-sensor": [
9426
- {
9427
- "path": "percent",
9428
- "valueType": "number"
9429
- },
9430
- {
9431
- "path": "unit",
9432
- "valueType": "string"
9433
- },
9434
- {
9435
- "path": "precision",
9436
- "valueType": "number"
9437
- }
9438
- ],
9506
+ "humidity-sensor": [{
9507
+ "path": "percent",
9508
+ "valueType": "number"
9509
+ }],
9439
9510
  "image": [{
9440
9511
  "path": "url",
9441
9512
  "valueType": "string"
@@ -9774,20 +9845,10 @@ Object.freeze({
9774
9845
  "valueType": "boolean"
9775
9846
  }
9776
9847
  ],
9777
- "numeric-sensor": [
9778
- {
9779
- "path": "value",
9780
- "valueType": "number"
9781
- },
9782
- {
9783
- "path": "unit",
9784
- "valueType": "string"
9785
- },
9786
- {
9787
- "path": "precision",
9788
- "valueType": "number"
9789
- }
9790
- ],
9848
+ "numeric-sensor": [{
9849
+ "path": "value",
9850
+ "valueType": "number"
9851
+ }],
9791
9852
  "pet-feeder": [
9792
9853
  {
9793
9854
  "path": "foodLevel",
@@ -9879,14 +9940,6 @@ Object.freeze({
9879
9940
  {
9880
9941
  "path": "powerFactor",
9881
9942
  "valueType": "number"
9882
- },
9883
- {
9884
- "path": "unit",
9885
- "valueType": "string"
9886
- },
9887
- {
9888
- "path": "precision",
9889
- "valueType": "number"
9890
9943
  }
9891
9944
  ],
9892
9945
  "presence": [
@@ -9915,20 +9968,10 @@ Object.freeze({
9915
9968
  "valueType": "number"
9916
9969
  }
9917
9970
  ],
9918
- "pressure-sensor": [
9919
- {
9920
- "path": "hpa",
9921
- "valueType": "number"
9922
- },
9923
- {
9924
- "path": "unit",
9925
- "valueType": "string"
9926
- },
9927
- {
9928
- "path": "precision",
9929
- "valueType": "number"
9930
- }
9931
- ],
9971
+ "pressure-sensor": [{
9972
+ "path": "hpa",
9973
+ "valueType": "number"
9974
+ }],
9932
9975
  "privacy-mask": [{
9933
9976
  "path": "enabled",
9934
9977
  "valueType": "boolean"
@@ -10239,20 +10282,10 @@ Object.freeze({
10239
10282
  "path": "tampered",
10240
10283
  "valueType": "boolean"
10241
10284
  }],
10242
- "temperature-sensor": [
10243
- {
10244
- "path": "celsius",
10245
- "valueType": "number"
10246
- },
10247
- {
10248
- "path": "unit",
10249
- "valueType": "string"
10250
- },
10251
- {
10252
- "path": "precision",
10253
- "valueType": "number"
10254
- }
10255
- ],
10285
+ "temperature-sensor": [{
10286
+ "path": "celsius",
10287
+ "valueType": "number"
10288
+ }],
10256
10289
  "update": [
10257
10290
  {
10258
10291
  "path": "currentVersion",
@@ -10363,10 +10396,6 @@ Object.freeze({
10363
10396
  "path": "temperature",
10364
10397
  "valueType": "number"
10365
10398
  },
10366
- {
10367
- "path": "temperatureUnit",
10368
- "valueType": "string"
10369
- },
10370
10399
  {
10371
10400
  "path": "humidity",
10372
10401
  "valueType": "number"
@@ -10375,18 +10404,10 @@ Object.freeze({
10375
10404
  "path": "pressure",
10376
10405
  "valueType": "number"
10377
10406
  },
10378
- {
10379
- "path": "pressureUnit",
10380
- "valueType": "string"
10381
- },
10382
10407
  {
10383
10408
  "path": "windSpeed",
10384
10409
  "valueType": "number"
10385
10410
  },
10386
- {
10387
- "path": "windSpeedUnit",
10388
- "valueType": "string"
10389
- },
10390
10411
  {
10391
10412
  "path": "windBearing",
10392
10413
  "valueType": "number"
@@ -10878,7 +10899,7 @@ function errMsg(err) {
10878
10899
  return String(err);
10879
10900
  }
10880
10901
  //#endregion
10881
- //#region ../types/dist/composition-kEl9mevu.mjs
10902
+ //#region ../types/dist/composition-OhpuespA.mjs
10882
10903
  /**
10883
10904
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10884
10905
  *
@@ -11633,15 +11654,74 @@ function validateExpressionSource(src) {
11633
11654
  }
11634
11655
  return null;
11635
11656
  }
11636
- /** A composed device's stableId is this prefix plus the block id. */
11637
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11638
- /** Longest snippet a `code` source may carry (slice 4). */
11639
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11657
+ /**
11658
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11659
+ * id. Split out so helper schemas can name it without a cycle through
11660
+ * `composition.ts` (D659).
11661
+ */
11640
11662
  var CompositionSourceRefSchema = object({
11641
11663
  /** The owning addon; `stableId` is unique only within it. */
11642
11664
  addonId: string().min(1),
11643
11665
  stableId: string().min(1)
11644
11666
  });
11667
+ var GROUP_TEMPLATE_IDS = [
11668
+ "temperature",
11669
+ "humidity",
11670
+ "pressure",
11671
+ "illuminance",
11672
+ "power",
11673
+ "energy",
11674
+ "battery",
11675
+ "numeric",
11676
+ "motion",
11677
+ "contact",
11678
+ "leak",
11679
+ "smoke",
11680
+ "occupancy",
11681
+ "binary",
11682
+ "switch",
11683
+ "light"
11684
+ ];
11685
+ var NUMERIC_AGGREGATES = [
11686
+ "mean",
11687
+ "median",
11688
+ "min",
11689
+ "max",
11690
+ "sum"
11691
+ ];
11692
+ var BINARY_AGGREGATES = ["any", "all"];
11693
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11694
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11695
+ var AGGREGATE_REDUCERS = [
11696
+ "mean",
11697
+ "median",
11698
+ "min",
11699
+ "max",
11700
+ "sum",
11701
+ "any",
11702
+ "all"
11703
+ ];
11704
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11705
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11706
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11707
+ /** One member. The field is stored so occupancy can name motion or binary. */
11708
+ var HelperMemberSchema = object({
11709
+ source: CompositionSourceRefSchema,
11710
+ cap: string().min(1),
11711
+ fieldPath: string().min(1)
11712
+ });
11713
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11714
+ kind: literal("group"),
11715
+ templateId: GroupTemplateIdSchema,
11716
+ aggregate: AggregateReducerSchema,
11717
+ members: array(HelperMemberSchema).min(1).max(64),
11718
+ /** Actuator groups only. Readings leave this unset. */
11719
+ active: ActiveCriterionSchema.optional()
11720
+ })]);
11721
+ /** A composed device's stableId is this prefix plus the block id. */
11722
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11723
+ /** Longest snippet a `code` source may carry (slice 4). */
11724
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11645
11725
  /** One field of one capability of one source device. */
11646
11726
  var CompositionFieldReadSchema = object({
11647
11727
  source: CompositionSourceRefSchema,
@@ -11685,7 +11765,33 @@ var CompositionCodeSourceSchema = object({
11685
11765
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11686
11766
  CompositionFromSourceSchema,
11687
11767
  CompositionExpressionSourceSchema,
11688
- CompositionCodeSourceSchema
11768
+ CompositionCodeSourceSchema,
11769
+ object({
11770
+ kind: literal("aggregate"),
11771
+ reducer: _enum([
11772
+ "mean",
11773
+ "median",
11774
+ "min",
11775
+ "max",
11776
+ "sum",
11777
+ "any",
11778
+ "all"
11779
+ ]),
11780
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11781
+ /** `value` is the reading; the others are the member counts. */
11782
+ publish: _enum([
11783
+ "value",
11784
+ "included",
11785
+ "excluded",
11786
+ "total"
11787
+ ]),
11788
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11789
+ empty: _enum([
11790
+ "null",
11791
+ "omit",
11792
+ "offline"
11793
+ ])
11794
+ })
11689
11795
  ]);
11690
11796
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11691
11797
  kind: literal("forward"),
@@ -11731,7 +11837,13 @@ var CompositionSchema = object({
11731
11837
  kind: literal("existing"),
11732
11838
  device: CompositionSourceRefSchema
11733
11839
  })]),
11734
- features: array(CompositionFeatureSchema).min(1).max(16)
11840
+ features: array(CompositionFeatureSchema).min(1).max(16),
11841
+ /**
11842
+ * Present when the block is a helper template. Absent on a free
11843
+ * composition (the room example, device 4603). The editor reads this,
11844
+ * never the compiled features.
11845
+ */
11846
+ helper: HelperSpecSchema.optional()
11735
11847
  }).superRefine((composition, ctx) => {
11736
11848
  const seen = /* @__PURE__ */ new Set();
11737
11849
  composition.features.forEach((feature, index) => {
@@ -17971,7 +18083,11 @@ var CompositionProblemSchema = object({
17971
18083
  "item-field-reserved",
17972
18084
  "target-missing",
17973
18085
  "target-unreadable",
17974
- "not-replaceable"
18086
+ "not-replaceable",
18087
+ "native-capability-replace",
18088
+ "mixed-quantity",
18089
+ "helper-mismatch",
18090
+ "member-constraint"
17975
18091
  ]),
17976
18092
  path: string(),
17977
18093
  message: string()
@@ -17992,7 +18108,8 @@ var CompositionUnavailableModeSchema = _enum([
17992
18108
  "null",
17993
18109
  "offline",
17994
18110
  "unknown",
17995
- "release"
18111
+ "release",
18112
+ "omit"
17996
18113
  ]);
17997
18114
  /**
17998
18115
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18029,9 +18146,11 @@ var CompositionFieldPlanSchema = object({
18029
18146
  reads: array(CompositionFieldReadSchema)
18030
18147
  });
18031
18148
  /**
18032
- * What a feature does to its target (D663): `new` builds a capability on a new
18033
- * device; on an existing device it `add`s a capability the device lacks, or
18034
- * `replace`s named fields of one it has natively.
18149
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18150
+ * new device; on an existing device it `add`s a capability the device lacks.
18151
+ * Replacing a capability the device already serves is refused
18152
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18153
+ * wire for a claim the hub already holds.
18035
18154
  */
18036
18155
  var CompositionFeatureModeSchema = _enum([
18037
18156
  "new",
@@ -18204,6 +18323,275 @@ Object.freeze({
18204
18323
  result: ["number", "null"]
18205
18324
  }
18206
18325
  });
18326
+ /**
18327
+ * One catalog row per base reading a group helper can build (D944 §3).
18328
+ * Data, not a form default written into the block.
18329
+ */
18330
+ var NOW = {
18331
+ fieldPath: "lastFetchedAt",
18332
+ expr: "now"
18333
+ };
18334
+ var CHANGED = {
18335
+ fieldPath: "lastChangedAt",
18336
+ expr: "now"
18337
+ };
18338
+ function numeric(input) {
18339
+ return {
18340
+ ...input,
18341
+ family: "numeric",
18342
+ deviceType: DeviceType.Sensor,
18343
+ memberDeviceType: DeviceType.Sensor,
18344
+ commandCaps: [],
18345
+ baseUnit: baseUnitOf(input.quantity),
18346
+ aggregates: NUMERIC_AGGREGATES,
18347
+ accept: [{
18348
+ cap: input.outputCap,
18349
+ fieldPath: input.outputField
18350
+ }]
18351
+ };
18352
+ }
18353
+ function binary(input) {
18354
+ return {
18355
+ ...input,
18356
+ family: "binary",
18357
+ deviceType: DeviceType.Sensor,
18358
+ memberDeviceType: DeviceType.Sensor,
18359
+ commandCaps: [],
18360
+ quantity: "none",
18361
+ baseUnit: void 0,
18362
+ aggregates: BINARY_AGGREGATES,
18363
+ empty: "offline"
18364
+ };
18365
+ }
18366
+ var GROUP_TEMPLATES = [
18367
+ numeric({
18368
+ id: "temperature",
18369
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18370
+ role: DeviceRole.TemperatureSensor,
18371
+ outputCap: "temperature-sensor",
18372
+ outputField: "celsius",
18373
+ quantity: "temperature",
18374
+ defaultAggregate: "mean",
18375
+ empty: "null",
18376
+ companions: [NOW]
18377
+ }),
18378
+ numeric({
18379
+ id: "humidity",
18380
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18381
+ role: DeviceRole.HumiditySensor,
18382
+ outputCap: "humidity-sensor",
18383
+ outputField: "percent",
18384
+ quantity: "humidity",
18385
+ defaultAggregate: "mean",
18386
+ empty: "null",
18387
+ companions: [NOW]
18388
+ }),
18389
+ numeric({
18390
+ id: "pressure",
18391
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18392
+ role: DeviceRole.PressureSensor,
18393
+ outputCap: "pressure-sensor",
18394
+ outputField: "hpa",
18395
+ quantity: "pressure",
18396
+ defaultAggregate: "mean",
18397
+ empty: "null",
18398
+ companions: [NOW]
18399
+ }),
18400
+ numeric({
18401
+ id: "illuminance",
18402
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18403
+ role: DeviceRole.AmbientLightSensor,
18404
+ outputCap: "ambient-light-sensor",
18405
+ outputField: "lux",
18406
+ quantity: "illuminance",
18407
+ defaultAggregate: "mean",
18408
+ empty: "null",
18409
+ companions: [NOW]
18410
+ }),
18411
+ numeric({
18412
+ id: "power",
18413
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18414
+ role: DeviceRole.PowerSensor,
18415
+ outputCap: "power-meter",
18416
+ outputField: "watts",
18417
+ quantity: "power",
18418
+ defaultAggregate: "sum",
18419
+ empty: "omit",
18420
+ companions: [NOW]
18421
+ }),
18422
+ numeric({
18423
+ id: "energy",
18424
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18425
+ role: DeviceRole.EnergySensor,
18426
+ outputCap: "power-meter",
18427
+ outputField: "kwhTotal",
18428
+ quantity: "energy",
18429
+ defaultAggregate: "sum",
18430
+ empty: "omit",
18431
+ companions: [NOW]
18432
+ }),
18433
+ numeric({
18434
+ id: "battery",
18435
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18436
+ role: DeviceRole.BatterySensor,
18437
+ outputCap: "battery",
18438
+ outputField: "percentage",
18439
+ quantity: "percentage",
18440
+ defaultAggregate: "mean",
18441
+ empty: "null",
18442
+ companions: [
18443
+ {
18444
+ fieldPath: "charging",
18445
+ expr: "\"unknown\""
18446
+ },
18447
+ {
18448
+ fieldPath: "sleeping",
18449
+ expr: "false"
18450
+ },
18451
+ {
18452
+ fieldPath: "lastUpdated",
18453
+ expr: "now"
18454
+ }
18455
+ ]
18456
+ }),
18457
+ numeric({
18458
+ id: "numeric",
18459
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18460
+ role: DeviceRole.NumericSensor,
18461
+ outputCap: "numeric-sensor",
18462
+ outputField: "value",
18463
+ quantity: "device",
18464
+ defaultAggregate: "mean",
18465
+ empty: "null",
18466
+ companions: [NOW]
18467
+ }),
18468
+ binary({
18469
+ id: "motion",
18470
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18471
+ role: DeviceRole.MotionSensor,
18472
+ outputCap: "motion",
18473
+ outputField: "detected",
18474
+ defaultAggregate: "any",
18475
+ accept: [{
18476
+ cap: "motion",
18477
+ fieldPath: "detected"
18478
+ }],
18479
+ companions: [{
18480
+ fieldPath: "lastDetectedAt",
18481
+ expr: "null"
18482
+ }, {
18483
+ fieldPath: "autoClearAfterMs",
18484
+ expr: "null"
18485
+ }]
18486
+ }),
18487
+ binary({
18488
+ id: "contact",
18489
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18490
+ role: DeviceRole.ContactSensor,
18491
+ outputCap: "contact",
18492
+ outputField: "entryOpen",
18493
+ defaultAggregate: "any",
18494
+ accept: [{
18495
+ cap: "contact",
18496
+ fieldPath: "entryOpen"
18497
+ }],
18498
+ companions: [CHANGED]
18499
+ }),
18500
+ binary({
18501
+ id: "leak",
18502
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18503
+ role: DeviceRole.LeakSensor,
18504
+ outputCap: "flood",
18505
+ outputField: "flooded",
18506
+ defaultAggregate: "any",
18507
+ accept: [{
18508
+ cap: "flood",
18509
+ fieldPath: "flooded"
18510
+ }],
18511
+ companions: [CHANGED]
18512
+ }),
18513
+ binary({
18514
+ id: "smoke",
18515
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18516
+ role: DeviceRole.SmokeSensor,
18517
+ outputCap: "smoke",
18518
+ outputField: "detected",
18519
+ defaultAggregate: "any",
18520
+ accept: [{
18521
+ cap: "smoke",
18522
+ fieldPath: "detected"
18523
+ }],
18524
+ companions: [CHANGED]
18525
+ }),
18526
+ binary({
18527
+ id: "occupancy",
18528
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18529
+ role: DeviceRole.BinarySensor,
18530
+ outputCap: "binary",
18531
+ outputField: "on",
18532
+ defaultAggregate: "any",
18533
+ accept: [{
18534
+ cap: "motion",
18535
+ fieldPath: "detected"
18536
+ }, {
18537
+ cap: "binary",
18538
+ fieldPath: "on"
18539
+ }],
18540
+ companions: [CHANGED]
18541
+ }),
18542
+ binary({
18543
+ id: "binary",
18544
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18545
+ role: DeviceRole.BinarySensor,
18546
+ outputCap: "binary",
18547
+ outputField: "on",
18548
+ defaultAggregate: "any",
18549
+ accept: [{
18550
+ cap: "binary",
18551
+ fieldPath: "on"
18552
+ }],
18553
+ companions: [CHANGED]
18554
+ }),
18555
+ actuator({
18556
+ id: "switch",
18557
+ 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.",
18558
+ deviceType: DeviceType.Switch,
18559
+ outputCap: "switch",
18560
+ commandCaps: ["switch"]
18561
+ }),
18562
+ actuator({
18563
+ id: "light",
18564
+ 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.",
18565
+ deviceType: DeviceType.Light,
18566
+ outputCap: "switch",
18567
+ commandCaps: [
18568
+ "switch",
18569
+ "brightness",
18570
+ "color"
18571
+ ]
18572
+ })
18573
+ ];
18574
+ function actuator(input) {
18575
+ return {
18576
+ ...input,
18577
+ family: "actuator",
18578
+ role: void 0,
18579
+ memberDeviceType: input.deviceType,
18580
+ outputField: "on",
18581
+ quantity: "none",
18582
+ baseUnit: void 0,
18583
+ aggregates: BINARY_AGGREGATES,
18584
+ defaultAggregate: "any",
18585
+ empty: "offline",
18586
+ accept: [{
18587
+ cap: "switch",
18588
+ fieldPath: "on"
18589
+ }],
18590
+ companions: [CHANGED]
18591
+ };
18592
+ }
18593
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18594
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18207
18595
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18208
18596
  /**
18209
18597
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19917,12 +20305,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19917
20305
  * providers detect renames by matching `uniqueId` and patch `id`
19918
20306
  * in-place so dispatch keeps working.
19919
20307
  *
19920
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19921
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19922
- * state push and are the single source of truth. The `device_class` tag is
19923
- * an HA-specific input that is consumed at the discovery boundary
19924
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19925
- * stored after that point.
20308
+ * **Display unit and precision are NOT stored here.** They are a read-time
20309
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20310
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20311
+ * neither is repeated on each sample. The `device_class` tag is an
20312
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20313
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19926
20314
  *
19927
20315
  * The `raw` escape hatch carries provider-specific attributes that don't
19928
20316
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19936,6 +20324,14 @@ var SourceInfoSchema = object({
19936
20324
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19937
20325
  * Used to detect rename when `id` changes. */
19938
20326
  uniqueId: string().optional(),
20327
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20328
+ quantity: ReadingQuantitySchema.optional(),
20329
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20330
+ dateTimeShape: _enum([
20331
+ "date",
20332
+ "time",
20333
+ "datetime"
20334
+ ]).optional(),
19939
20335
  /** Provider-specific extras that don't fit the structured slots.
19940
20336
  * Whitelist what goes here per provider — do NOT dump entire HA
19941
20337
  * attribute blobs (would explode the meta JSON column). */
@@ -41040,7 +41436,8 @@ method(object({
41040
41436
  "enum"
41041
41437
  ]),
41042
41438
  enumValues: array(string()).optional(),
41043
- item: boolean().optional()
41439
+ item: boolean().optional(),
41440
+ quantity: ReadingQuantitySchema.optional()
41044
41441
  })).readonly(),
41045
41442
  itemArray: object({
41046
41443
  path: string(),
@@ -44884,8 +45281,8 @@ var MaskPolygonShapeSchema = object({
44884
45281
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44885
45282
  var MaskGridShapeSchema = object({
44886
45283
  kind: literal("grid"),
44887
- gridWidth: number(),
44888
- gridHeight: number(),
45284
+ gridWidth: number().meta(readingMeta("none")),
45285
+ gridHeight: number().meta(readingMeta("none")),
44889
45286
  cells: array(boolean())
44890
45287
  });
44891
45288
  discriminatedUnion("kind", [
@@ -44997,6 +45394,8 @@ var NcSystemEventKindSchema = _enum([
44997
45394
  "detection-blind",
44998
45395
  "motion-noisy",
44999
45396
  "motion-calm",
45397
+ "package-llm-silent",
45398
+ "package-llm-answered",
45000
45399
  "recording-stalled",
45001
45400
  "recording-healthy",
45002
45401
  "recording-degraded",
@@ -56192,6 +56591,28 @@ var PackageVersionInfoSchema = object({
56192
56591
  deprecated: string().optional(),
56193
56592
  distTags: array(string()).readonly()
56194
56593
  });
56594
+ /**
56595
+ * One published document. `ok` is markdown from the registry. `missing` means
56596
+ * the registry answered and the file was not there. `error` is a registry or
56597
+ * tarball failure — never an empty string standing in for either.
56598
+ */
56599
+ var PackageDocBodySchema = discriminatedUnion("status", [
56600
+ object({
56601
+ status: literal("ok"),
56602
+ markdown: string()
56603
+ }),
56604
+ object({ status: literal("missing") }),
56605
+ object({
56606
+ status: literal("error"),
56607
+ message: string()
56608
+ })
56609
+ ]);
56610
+ var PackageDocsSchema = object({
56611
+ name: string(),
56612
+ version: string(),
56613
+ readme: PackageDocBodySchema,
56614
+ changelog: PackageDocBodySchema
56615
+ });
56195
56616
  var SearchResultSchema = object({
56196
56617
  name: string(),
56197
56618
  version: string(),
@@ -56410,7 +56831,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56410
56831
  }), object({ success: literal(true) }), {
56411
56832
  kind: "mutation",
56412
56833
  auth: "admin"
56413
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56834
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56835
+ name: string().min(1),
56836
+ version: string().min(1).optional()
56837
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56414
56838
  kind: "mutation",
56415
56839
  auth: "admin"
56416
56840
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56469,27 +56893,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56469
56893
  */
56470
56894
  var AirQualitySensorStatusSchema = object({
56471
56895
  /** Carbon dioxide concentration in ppm. */
56472
- co2Ppm: number().min(0).optional(),
56896
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56473
56897
  /** Total volatile organic compounds in ppb. */
56474
- vocPpb: number().min(0).optional(),
56898
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56475
56899
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56476
- pm25: number().min(0).optional(),
56900
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56477
56901
  /** Particulate matter ≤ 10 μm in µg/m³. */
56478
- pm10: number().min(0).optional(),
56902
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56479
56903
  /** Composite AQI value (typically 0..500). */
56480
- aqi: number().optional(),
56904
+ aqi: number().optional().meta(readingMeta("index")),
56481
56905
  /** Ms epoch when the slice was last updated. */
56482
- lastFetchedAt: number(),
56483
- /** Live display unit of the single metric this slice carries (e.g. HA
56484
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56485
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56486
- * per slice is unambiguous. */
56487
- unit: string().optional(),
56488
- /** Suggested decimal places for numeric display.
56489
- * Populated live from the upstream source when provided (e.g. HA
56490
- * `attributes.suggested_display_precision`). Falls back to
56491
- * auto-formatting when absent. */
56492
- precision: number().int().min(0).max(10).optional()
56906
+ lastFetchedAt: number()
56493
56907
  });
56494
56908
  var airQualitySensorCapability = {
56495
56909
  name: "air-quality-sensor",
@@ -56526,12 +56940,12 @@ var airQualitySensorCapability = {
56526
56940
  },
56527
56941
  pm25: {
56528
56942
  kind: "gauge",
56529
- unit: "ug/m3",
56943
+ unit: "µg/m³",
56530
56944
  deadband: 1
56531
56945
  },
56532
56946
  pm10: {
56533
56947
  kind: "gauge",
56534
- unit: "ug/m3",
56948
+ unit: "µg/m³",
56535
56949
  deadband: 1
56536
56950
  },
56537
56951
  aqi: {
@@ -56549,22 +56963,13 @@ var airQualitySensorCapability = {
56549
56963
  * entries with `device_class: illuminance`.
56550
56964
  */
56551
56965
  var AmbientLightSensorStatusSchema = object({
56552
- /** Current illuminance in lux (lx). */
56553
- lux: number().min(0).meta({
56554
- unit: "lx",
56966
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56967
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56968
+ precision: 0,
56555
56969
  label: "Illuminance"
56556
- }),
56970
+ })),
56557
56971
  /** Ms epoch when the slice was last updated. */
56558
- lastFetchedAt: number(),
56559
- /** Live display unit from the upstream source (e.g. HA
56560
- * `attributes.unit_of_measurement`). The UI prefers this over the
56561
- * role's canonical unit. Absent → fall back to the canonical unit. */
56562
- unit: string().optional(),
56563
- /** Suggested decimal places for numeric display.
56564
- * Populated live from the upstream source when provided (e.g. HA
56565
- * `attributes.suggested_display_precision`). Falls back to
56566
- * auto-formatting when absent. */
56567
- precision: number().int().min(0).max(10).optional()
56972
+ lastFetchedAt: number()
56568
56973
  });
56569
56974
  var ambientLightSensorCapability = {
56570
56975
  name: "ambient-light-sensor",
@@ -56596,12 +57001,7 @@ var ambientLightSensorCapability = {
56596
57001
  } } },
56597
57002
  /** Clock fields: written, but excluded from the compare that decides
56598
57003
  * whether persisting is worth a SQLite commit. */
56599
- volatileStateFields: ["lastFetchedAt"],
56600
- customize: {
56601
- version: 1,
56602
- advanced: "derive",
56603
- targets: []
56604
- }
57004
+ volatileStateFields: ["lastFetchedAt"]
56605
57005
  };
56606
57006
  /**
56607
57007
  * Per-class audio metrics aggregated over a sliding window.
@@ -56828,10 +57228,7 @@ var BatteryStatusSchema = object({
56828
57228
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56829
57229
  * declines to band a non-finite reading, which is the correct shape.
56830
57230
  */
56831
- percentage: number().min(0).max(100).nullable().meta({
56832
- unit: "%",
56833
- label: "Charge"
56834
- }),
57231
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56835
57232
  /**
56836
57233
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56837
57234
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56858,7 +57255,14 @@ var BatteryStatusSchema = object({
56858
57255
  */
56859
57256
  sleeping: boolean().meta({ label: "Sleeping" }),
56860
57257
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56861
- lastUpdated: number(),
57258
+ lastUpdated: number().meta(readingMeta("none")),
57259
+ /**
57260
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57261
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57262
+ * that only report charge. A second reading on this cap, not a second
57263
+ * `numeric-sensor` on the same device.
57264
+ */
57265
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56862
57266
  /**
56863
57267
  * Ms epoch of the last time the device PROVED it was reachable — a
56864
57268
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56910,7 +57314,6 @@ var BATTERY_DEVICE_TYPES = [
56910
57314
  /** Generic Customize target for the percentage reading (D936). */
56911
57315
  var batteryCustomize = {
56912
57316
  version: 1,
56913
- advanced: "derive",
56914
57317
  targets: [{
56915
57318
  id: "percentage",
56916
57319
  cardinality: "scalar",
@@ -56919,7 +57322,7 @@ var batteryCustomize = {
56919
57322
  stateField: "percentage"
56920
57323
  },
56921
57324
  deviceTypes: BATTERY_DEVICE_TYPES,
56922
- apply: "add-or-replace",
57325
+ apply: "add",
56923
57326
  path: "percentage",
56924
57327
  sources: [{
56925
57328
  kind: "field",
@@ -57040,10 +57443,16 @@ onStatusChanged: { data: object({
57040
57443
  /**
57041
57444
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57042
57445
  */
57043
- history: { fields: { percentage: {
57044
- kind: "gauge",
57045
- unit: "%"
57046
- } } },
57446
+ history: { fields: {
57447
+ percentage: {
57448
+ kind: "gauge",
57449
+ unit: "%"
57450
+ },
57451
+ volts: {
57452
+ kind: "gauge",
57453
+ unit: "V"
57454
+ }
57455
+ } },
57047
57456
  customize: batteryCustomize,
57048
57457
  /** Clock fields: written, but excluded from the compare that decides
57049
57458
  * whether persisting is worth a SQLite commit. */
@@ -57090,12 +57499,7 @@ var binaryCapability = {
57090
57499
  history: { fields: { on: { kind: "boolean" } } },
57091
57500
  /** Clock fields: written, but excluded from the compare that decides
57092
57501
  * whether persisting is worth a SQLite commit. */
57093
- volatileStateFields: ["lastChangedAt"],
57094
- customize: {
57095
- version: 1,
57096
- advanced: "derive",
57097
- targets: []
57098
- }
57502
+ volatileStateFields: ["lastChangedAt"]
57099
57503
  };
57100
57504
  /**
57101
57505
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57111,7 +57515,7 @@ var binaryCapability = {
57111
57515
  */
57112
57516
  var BrightnessStatusSchema = object({
57113
57517
  /** Current level as 0..100 inclusive. Firmware-reported. */
57114
- percentage: number().min(0).max(100),
57518
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57115
57519
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57116
57520
  lastChangedAt: number()
57117
57521
  });
@@ -57565,21 +57969,21 @@ var ClimateControlStatusSchema = object({
57565
57969
  /** Single setpoint in Celsius. Used by single-target modes
57566
57970
  * (heat / cool / dry). Null when the device is in a range mode
57567
57971
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57568
- target: number().nullable(),
57972
+ target: number().nullable().meta(readingMeta("temperature")),
57569
57973
  /** Upper bound of the dual setpoint in Celsius. Populated only
57570
57974
  * in `heat_cool` mode. */
57571
- targetHigh: number().nullable(),
57975
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57572
57976
  /** Lower bound of the dual setpoint in Celsius. Populated only
57573
57977
  * in `heat_cool` mode. */
57574
- targetLow: number().nullable(),
57978
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57575
57979
  /** Target relative humidity (0..100). Null when the device has no
57576
57980
  * humidity control. */
57577
- targetHumidity: number().min(0).max(100).nullable(),
57981
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57578
57982
  /** Current measured temperature in Celsius. Read-only — pushed by
57579
57983
  * the device's internal sensor. */
57580
- currentTemp: number().nullable(),
57984
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57581
57985
  /** Current measured relative humidity (0..100). Read-only. */
57582
- currentHumidity: number().min(0).max(100).nullable(),
57986
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57583
57987
  /** Vertical louver swing on/off. Null when the device has no vertical
57584
57988
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57585
57989
  swingVertical: boolean().nullable(),
@@ -57587,11 +57991,11 @@ var ClimateControlStatusSchema = object({
57587
57991
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57588
57992
  swingHorizontal: boolean().nullable(),
57589
57993
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57590
- minTemp: number().nullable().optional(),
57994
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57591
57995
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57592
- maxTemp: number().nullable().optional(),
57996
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57593
57997
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57594
- tempStep: number().positive().nullable().optional(),
57998
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57595
57999
  /** Ms epoch when the slice was last updated (push or command). */
57596
58000
  lastFetchedAt: number()
57597
58001
  });
@@ -57733,17 +58137,17 @@ var climateControlCapability = {
57733
58137
  * modes to expose in the picker without a slice fetch.
57734
58138
  */
57735
58139
  var RgbTripletSchema = object({
57736
- r: number().int().min(0).max(255),
57737
- g: number().int().min(0).max(255),
57738
- b: number().int().min(0).max(255)
58140
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58141
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58142
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57739
58143
  });
57740
58144
  var HsvTripletSchema = object({
57741
58145
  /** Hue in degrees, 0..360. */
57742
- h: number().min(0).max(360),
58146
+ h: number().min(0).max(360).meta(readingMeta("none")),
57743
58147
  /** Saturation as 0..100 inclusive. */
57744
- s: number().min(0).max(100),
58148
+ s: number().min(0).max(100).meta(readingMeta("none")),
57745
58149
  /** Value/brightness as 0..100 inclusive. */
57746
- v: number().min(0).max(100)
58150
+ v: number().min(0).max(100).meta(readingMeta("none"))
57747
58151
  });
57748
58152
  var ColorInputSchema = discriminatedUnion("mode", [
57749
58153
  object({
@@ -57772,7 +58176,7 @@ var ColorStatusSchema = object({
57772
58176
  /** Populated when `mode === 'hsv'`. */
57773
58177
  hsv: HsvTripletSchema.optional(),
57774
58178
  /** Populated when `mode === 'mired'`. */
57775
- mireds: number().int().optional(),
58179
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57776
58180
  /** Ms epoch of the last operator-driven change. */
57777
58181
  lastChangedAt: number()
57778
58182
  });
@@ -57975,18 +58379,12 @@ _enum([
57975
58379
  "empty",
57976
58380
  "full"
57977
58381
  ]);
57978
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57979
- * each. */
57980
- var ConsumableRemainingUnitSchema = _enum([
57981
- "days",
57982
- "hours",
57983
- "cycles"
57984
- ]);
57985
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57986
- * days). `value` null = unknown (D393). */
58382
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58383
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58384
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57987
58385
  var ConsumableRemainingSchema = object({
57988
- value: number().min(0).nullable(),
57989
- unit: ConsumableRemainingUnitSchema
58386
+ value: number().min(0).nullable().meta(readingMeta("device")),
58387
+ kind: ConsumableRemainingKindSchema
57990
58388
  });
57991
58389
  /** A single consumable item. Either a continuous `level` (remaining
57992
58390
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57999,7 +58397,7 @@ var ConsumableItemSchema = object({
57999
58397
  /** Display name. */
58000
58398
  label: string().min(1),
58001
58399
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58002
- level: number().min(0).max(100).nullable(),
58400
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58003
58401
  /** The provider's discrete verdict when known (binary mode). */
58004
58402
  status: ConsumableStatusSchema.nullable(),
58005
58403
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58081,8 +58479,8 @@ var consumablesCustomize = {
58081
58479
  transforms: [{ kind: "identity" }]
58082
58480
  }],
58083
58481
  literals: [{
58084
- path: "remaining.unit",
58085
- values: [...ConsumableRemainingUnitSchema.options]
58482
+ path: "remaining.kind",
58483
+ values: [...ConsumableRemainingKindSchema.options]
58086
58484
  }]
58087
58485
  },
58088
58486
  {
@@ -58211,7 +58609,6 @@ reset: method(object({
58211
58609
  },
58212
58610
  "remaining.value": {
58213
58611
  kind: "gauge",
58214
- unitFrom: "remaining.unit",
58215
58612
  deadband: 1,
58216
58613
  displayName: "Remaining"
58217
58614
  }
@@ -58281,10 +58678,9 @@ var contactCapability = {
58281
58678
  * (empty array for the other kinds). It lives in the slice because
58282
58679
  * HA `select.*` entities may update their options at runtime.
58283
58680
  *
58284
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58285
- * `kind === 'numeric'` and are absent (undefined) for all other
58286
- * kinds. They are populated live from HA attributes on each state
58287
- * push — consistent with `options` and the sensor-unit approach.
58681
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58682
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58683
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58288
58684
  */
58289
58685
  var ControlStatusSchema = object({
58290
58686
  kind: _enum([
@@ -58296,39 +58692,17 @@ var ControlStatusSchema = object({
58296
58692
  /** Current value. Numeric kind → number; select/text/datetime → string.
58297
58693
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58298
58694
  * or time-only — HA accepts all three forms). */
58299
- value: union([number(), string()]),
58695
+ value: union([number().meta(readingMeta("device")), string()]),
58300
58696
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58301
58697
  options: array(string()),
58302
58698
  /** Ms epoch when the slice was last updated. */
58303
58699
  lastChangedAt: number(),
58304
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58305
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58306
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58307
- unit: string().optional(),
58308
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58309
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58310
- min: number().optional(),
58311
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58312
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58313
- max: number().optional(),
58314
- /** Step increment. Populated live from HA `attributes.step`.
58315
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58316
- step: number().optional(),
58317
- /** Suggested decimal places for numeric display. Populated live from HA
58318
- * `attributes.suggested_display_precision`, or derived from `step` when the
58319
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58320
- * other kinds. Falls back to auto-formatting when absent. */
58321
- precision: number().int().min(0).max(10).optional(),
58322
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58323
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58324
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58325
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58326
- * `has_date`/`has_time` attributes on each push. */
58327
- format: _enum([
58328
- "date",
58329
- "time",
58330
- "datetime"
58331
- ]).optional()
58700
+ /** Minimum allowed value, in the device quantity's base unit. */
58701
+ min: number().optional().meta(readingMeta("device")),
58702
+ /** Maximum allowed value, in the device quantity's base unit. */
58703
+ max: number().optional().meta(readingMeta("device")),
58704
+ /** Step increment, in the device quantity's base unit. */
58705
+ step: number().optional().meta(readingMeta("device"))
58332
58706
  });
58333
58707
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58334
58708
  object({
@@ -58394,9 +58768,9 @@ var CoverStatusSchema = object({
58394
58768
  ]),
58395
58769
  /** 0 = fully closed, 100 = fully open. Null when the device has
58396
58770
  * no intermediate position surface. */
58397
- position: number().min(0).max(100).nullable(),
58771
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58398
58772
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58399
- tiltPosition: number().min(0).max(100).nullable(),
58773
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58400
58774
  /** Ms epoch when the slice was last updated. */
58401
58775
  lastChangedAt: number()
58402
58776
  });
@@ -58499,9 +58873,9 @@ var NormalizedRangeSchema$1 = object({
58499
58873
  var DayNightStatusSchema = object({
58500
58874
  mode: DayNightModeSchema,
58501
58875
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58502
- sensitivity: number().optional(),
58876
+ sensitivity: number().optional().meta(readingMeta("none")),
58503
58877
  /** Delay before the IR-cut filter flips, in seconds. */
58504
- switchDelaySec: number().optional(),
58878
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58505
58879
  lastFetchedAt: number()
58506
58880
  });
58507
58881
  /**
@@ -58719,11 +59093,11 @@ var DishwasherStatusSchema = object({
58719
59093
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58720
59094
  availablePrograms: array(string()),
58721
59095
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58722
- remainingSeconds: number().int().nonnegative().nullable(),
59096
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58723
59097
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58724
- delayedStartSeconds: number().int().nonnegative().nullable(),
59098
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58725
59099
  /** Cycle completion 0..100, or null when not applicable. */
58726
- progressPercent: number().min(0).max(100).nullable(),
59100
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58727
59101
  /** True when the door is reported open; null when the door state is not known. */
58728
59102
  doorOpen: boolean().nullable(),
58729
59103
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58747,7 +59121,7 @@ var DishwasherStatusSchema = object({
58747
59121
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58748
59122
  machineCareDue: boolean().nullable(),
58749
59123
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58750
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59124
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58751
59125
  /** Option states: null when the appliance does not expose the option or has not said. */
58752
59126
  halfLoad: boolean().nullable(),
58753
59127
  hygienePlus: boolean().nullable(),
@@ -58762,7 +59136,7 @@ var DishwasherStatusSchema = object({
58762
59136
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58763
59137
  activeEvents: array(string()),
58764
59138
  /** Numeric error code when the appliance reports one; null otherwise. */
58765
- errorCode: number().nullable(),
59139
+ errorCode: number().nullable().meta(readingMeta("none")),
58766
59140
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58767
59141
  errorLabel: string().nullable(),
58768
59142
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58993,7 +59367,7 @@ var DoorbellStatusSchema = object({
58993
59367
  /** Ms epoch of the last press. null = never observed since this provider started. */
58994
59368
  lastPressedAt: number().nullable(),
58995
59369
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58996
- pressCountSinceStart: number()
59370
+ pressCountSinceStart: number().meta(readingMeta("count"))
58997
59371
  });
58998
59372
  var DoorbellPressEventSchema = object({
58999
59373
  deviceId: number(),
@@ -59058,17 +59432,7 @@ onPressed: {
59058
59432
  history: "none",
59059
59433
  customize: doorbellCustomize
59060
59434
  };
59061
- /**
59062
- * Enum-state sensor — a string value picked from a finite option set.
59063
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59064
- * states, weather conditions, contact-source channels, mode strings).
59065
- * The option set is stable across observations; it is persisted in the
59066
- * device config blob (via the control slice at adoption) and is NOT
59067
- * stored in `sourceInfo`.
59068
- */
59069
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59070
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59071
- var EnumSensorDateTimeFormatSchema = _enum([
59435
+ _enum([
59072
59436
  "date",
59073
59437
  "time",
59074
59438
  "datetime"
@@ -59076,12 +59440,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59076
59440
  var EnumSensorStatusSchema = object({
59077
59441
  value: string(),
59078
59442
  /**
59079
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59080
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59081
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59082
- * a time-only sensor → `'time'`.
59443
+ * Set when `value` is a timestamp rather than an enum state, so history
59444
+ * does not learn ISO strings as states. The display shape (date / time /
59445
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59083
59446
  */
59084
- format: EnumSensorDateTimeFormatSchema.optional(),
59447
+ timestamp: boolean().optional(),
59085
59448
  /** Ms epoch when the slice was last updated. */
59086
59449
  lastFetchedAt: number()
59087
59450
  });
@@ -59108,11 +59471,11 @@ var enumSensorCapability = {
59108
59471
  * History (D850): the value as an `enum` series (an appliance's program or
59109
59472
  * operation state, an HVAC action). The option set is per device, so the
59110
59473
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59111
- * `format` carries a timestamp, not a state, and is not recorded.
59474
+ * `timestamp` carries a clock, not a state, and is not recorded.
59112
59475
  */
59113
59476
  history: { fields: { value: {
59114
59477
  kind: "enum",
59115
- skipWhenSet: "format"
59478
+ skipWhenSet: "timestamp"
59116
59479
  } } },
59117
59480
  /** Clock fields: written, but excluded from the compare that decides
59118
59481
  * whether persisting is worth a SQLite commit. */
@@ -59132,13 +59495,13 @@ var EventFireSchema = object({
59132
59495
  deviceId: number(),
59133
59496
  eventType: string(),
59134
59497
  data: record(string(), unknown()).nullable(),
59135
- timestamp: number(),
59136
- seq: number()
59498
+ timestamp: number().meta(readingMeta("none")),
59499
+ seq: number().meta(readingMeta("count"))
59137
59500
  });
59138
59501
  var EventEmitterStatusSchema = object({
59139
59502
  eventTypes: array(string()),
59140
59503
  lastEvent: EventFireSchema.nullable(),
59141
- eventCountSinceStart: number()
59504
+ eventCountSinceStart: number().meta(readingMeta("count"))
59142
59505
  });
59143
59506
  var eventEmitterCapability = {
59144
59507
  name: "event-emitter",
@@ -59551,10 +59914,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59551
59914
  var FanControlStatusSchema = object({
59552
59915
  /** Active speed as 0..100 inclusive. Null when the device has no
59553
59916
  * speed surface (single-speed fan). */
59554
- percentage: number().min(0).max(100).nullable(),
59917
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59555
59918
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59556
59919
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59557
- percentageStep: number().positive().optional(),
59920
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59558
59921
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59559
59922
  * — empty string when no preset is active or supported. */
59560
59923
  preset: string(),
@@ -59675,7 +60038,7 @@ var FeatureProbeStatusSchema = object({
59675
60038
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59676
60039
  model: string().nullable(),
59677
60040
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59678
- channelCount: number().nullable(),
60041
+ channelCount: number().nullable().meta(readingMeta("count")),
59679
60042
  /**
59680
60043
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59681
60044
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59798,9 +60161,91 @@ var gasCapability = {
59798
60161
  * whether persisting is worth a SQLite commit. */
59799
60162
  volatileStateFields: ["lastChangedAt"]
59800
60163
  };
60164
+ var GroupCommandsSchema = object({
60165
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60166
+ results: array(object({
60167
+ deviceId: number().int().nonnegative(),
60168
+ cap: string().min(1),
60169
+ method: string().min(1),
60170
+ ok: boolean(),
60171
+ error: string().optional(),
60172
+ /** Ms epoch of the attempt. */
60173
+ at: number()
60174
+ })) });
60175
+ var groupCommandsCapability = {
60176
+ name: "group-commands",
60177
+ scope: "device",
60178
+ deviceNative: true,
60179
+ mode: "singleton",
60180
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60181
+ methods: {},
60182
+ status: {
60183
+ schema: GroupCommandsSchema,
60184
+ kind: "push"
60185
+ },
60186
+ runtimeState: GroupCommandsSchema,
60187
+ /**
60188
+ * Runtime-state durability: **session** — the last attempt. A restored
60189
+ * failure would outlive the command that caused it.
60190
+ *
60191
+ * See `RuntimeStateDurability`. Enforced by
60192
+ * `scripts/check-runtime-state-durability.ts`.
60193
+ */
60194
+ durability: "session",
60195
+ history: "none"
60196
+ };
60197
+ /**
60198
+ * How many members of a helper group answered (D944). Counts are integers,
60199
+ * not measurements: zero included is a real count, never a stand-in reading.
60200
+ * The reading itself stays on the output capability. History is none — the
60201
+ * series that later statistics read is the output cap's own history block.
60202
+ */
60203
+ var GroupStatusSchema = object({
60204
+ /** Members whose current value entered the aggregate. */
60205
+ included: number().int().nonnegative().meta(readingMeta("count", {
60206
+ precision: 0,
60207
+ label: "Reporting"
60208
+ })),
60209
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60210
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60211
+ precision: 0,
60212
+ label: "Excluded"
60213
+ })),
60214
+ /** `included` + `excluded`. */
60215
+ total: number().int().nonnegative().meta(readingMeta("count", {
60216
+ precision: 0,
60217
+ label: "Members"
60218
+ }))
60219
+ });
60220
+ var groupStatusCapability = {
60221
+ name: "group-status",
60222
+ scope: "device",
60223
+ deviceNative: true,
60224
+ mode: "singleton",
60225
+ deviceTypes: [
60226
+ DeviceType.Sensor,
60227
+ DeviceType.Switch,
60228
+ DeviceType.Light
60229
+ ],
60230
+ methods: {},
60231
+ status: {
60232
+ schema: GroupStatusSchema,
60233
+ kind: "push"
60234
+ },
60235
+ runtimeState: GroupStatusSchema,
60236
+ /**
60237
+ * Runtime-state durability: **session** — recomputed from members on the
60238
+ * next source event; a restored count would outlive the readings it named.
60239
+ *
60240
+ * See `RuntimeStateDurability`. Enforced by
60241
+ * `scripts/check-runtime-state-durability.ts`.
60242
+ */
60243
+ durability: "session",
60244
+ history: "none"
60245
+ };
59801
60246
  var HeatingValveStatusSchema = object({
59802
60247
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59803
- position: number().min(0).max(100).nullable(),
60248
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59804
60249
  /** Ms epoch when the slice was last updated (push or poll). */
59805
60250
  lastFetchedAt: number()
59806
60251
  });
@@ -59843,9 +60288,9 @@ var HumidifierStatusSchema = object({
59843
60288
  /** Whether the humidifier is currently on. */
59844
60289
  on: boolean(),
59845
60290
  /** Current measured relative humidity (0..100). Null when not reported. */
59846
- currentHumidity: number().min(0).max(100).nullable(),
60291
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59847
60292
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59848
- targetHumidity: number().min(0).max(100).nullable(),
60293
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59849
60294
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59850
60295
  * device has no mode surface. */
59851
60296
  mode: string().nullable(),
@@ -59855,9 +60300,9 @@ var HumidifierStatusSchema = object({
59855
60300
  * `idle` / `off`). Null when the device doesn't report it. */
59856
60301
  action: string().nullable(),
59857
60302
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59858
- minHumidity: number().nullable(),
60303
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59859
60304
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59860
- maxHumidity: number().nullable(),
60305
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59861
60306
  /** Ms epoch when the slice was last updated. */
59862
60307
  lastChangedAt: number()
59863
60308
  });
@@ -59927,22 +60372,13 @@ var humidifierCapability = {
59927
60372
  * `sourceInfo`.
59928
60373
  */
59929
60374
  var HumiditySensorStatusSchema = object({
59930
- /** Current relative humidity, 0..100. */
59931
- percent: number().min(0).max(100).meta({
59932
- unit: "%",
60375
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60376
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60377
+ precision: 0,
59933
60378
  label: "Humidity"
59934
- }),
60379
+ })),
59935
60380
  /** Ms epoch when the slice was last updated. */
59936
- lastFetchedAt: number(),
59937
- /** Live display unit from the upstream source (e.g. HA
59938
- * `attributes.unit_of_measurement`). The UI prefers this over the
59939
- * role's canonical unit. Absent → fall back to the canonical unit. */
59940
- unit: string().optional(),
59941
- /** Suggested decimal places for numeric display.
59942
- * Populated live from the upstream source when provided (e.g. HA
59943
- * `attributes.suggested_display_precision`). Falls back to
59944
- * auto-formatting when absent. */
59945
- precision: number().int().min(0).max(10).optional()
60381
+ lastFetchedAt: number()
59946
60382
  });
59947
60383
  var humiditySensorCapability = {
59948
60384
  name: "humidity-sensor",
@@ -59973,12 +60409,7 @@ var humiditySensorCapability = {
59973
60409
  } } },
59974
60410
  /** Clock fields: written, but excluded from the compare that decides
59975
60411
  * whether persisting is worth a SQLite commit. */
59976
- volatileStateFields: ["lastFetchedAt"],
59977
- customize: {
59978
- version: 1,
59979
- advanced: "derive",
59980
- targets: []
59981
- }
60412
+ volatileStateFields: ["lastFetchedAt"]
59982
60413
  };
59983
60414
  /**
59984
60415
  * Image display cap. Models a single still image exposed by an integration —
@@ -59999,7 +60430,7 @@ var ImageStatusSchema = object({
59999
60430
  * entity exposes no `entity_picture` (yet). */
60000
60431
  url: string().nullable(),
60001
60432
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60002
- lastUpdated: number().nullable()
60433
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60003
60434
  });
60004
60435
  var imageCapability = {
60005
60436
  name: "image",
@@ -60087,19 +60518,19 @@ var NormalizedRangeSchema = object({
60087
60518
  */
60088
60519
  var ImageSettingsStatusSchema = object({
60089
60520
  /** Normalized 0–100. */
60090
- brightness: number().optional(),
60521
+ brightness: number().optional().meta(readingMeta("none")),
60091
60522
  /** Normalized 0–100. */
60092
- contrast: number().optional(),
60523
+ contrast: number().optional().meta(readingMeta("none")),
60093
60524
  /** Normalized 0–100. */
60094
- saturation: number().optional(),
60525
+ saturation: number().optional().meta(readingMeta("none")),
60095
60526
  /** Normalized 0–100. */
60096
- sharpness: number().optional(),
60527
+ sharpness: number().optional().meta(readingMeta("none")),
60097
60528
  mirror: boolean().optional(),
60098
60529
  flip: boolean().optional(),
60099
60530
  rotate: ImageRotateSchema.optional(),
60100
60531
  whiteBalance: WhiteBalanceModeSchema.optional(),
60101
60532
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60102
- warmth: number().optional(),
60533
+ warmth: number().optional().meta(readingMeta("none")),
60103
60534
  exposureMode: ExposureModeSchema.optional(),
60104
60535
  backlightMode: BacklightModeSchema.optional(),
60105
60536
  lastFetchedAt: number()
@@ -60378,10 +60809,10 @@ var IntercomAbilitySchema = object({
60378
60809
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60379
60810
  codecs: array(string()),
60380
60811
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60381
- sampleRate: number(),
60812
+ sampleRate: number().meta(readingMeta("none")),
60382
60813
  duplex: _enum(["half", "full"]),
60383
60814
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60384
- maxBacklogMs: number()
60815
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60385
60816
  });
60386
60817
  var IntercomStatusSchema = object({
60387
60818
  /** True while a talk session is open. */
@@ -60608,13 +61039,13 @@ var LawnMowerControlStatusSchema = object({
60608
61039
  activity: LawnMowerActivitySchema,
60609
61040
  /** 0..100 battery percentage. Null when the device has no battery
60610
61041
  * reading. */
60611
- batteryLevel: number().min(0).max(100).nullable(),
61042
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60612
61043
  /** 0..100 mowing-completion percentage of the current task, or null when no
60613
61044
  * task is active / progress is unavailable. */
60614
- progressPercent: number().min(0).max(100).nullable(),
61045
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60615
61046
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60616
61047
  * or null when unknown. */
60617
- currentCode: number().nullable(),
61048
+ currentCode: number().nullable().meta(readingMeta("none")),
60618
61049
  /** Human label for {@link currentCode}, or null when undecodable. */
60619
61050
  currentCodeLabel: string().nullable(),
60620
61051
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61147,7 +61578,7 @@ var MediaPlayerStatusSchema = object({
61147
61578
  state: MediaPlayerStateSchema,
61148
61579
  /** Volume as 0..100 inclusive. Null when the device has no volume
61149
61580
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61150
- volumeLevel: number().min(0).max(100).nullable(),
61581
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61151
61582
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61152
61583
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61153
61584
  isMuted: boolean().nullable(),
@@ -61160,10 +61591,10 @@ var MediaPlayerStatusSchema = object({
61160
61591
  currentMedia: MediaInfoSchema.nullable(),
61161
61592
  /** Current playback position in ms. Null when not seekable or
61162
61593
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61163
- positionMs: number().int().nonnegative().nullable(),
61594
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61164
61595
  /** Total duration of the current media in ms. Null when unknown
61165
61596
  * (live stream) or nothing is playing. */
61166
- durationMs: number().int().nonnegative().nullable(),
61597
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61167
61598
  /** Shuffle toggle. Null when no shuffle surface.
61168
61599
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61169
61600
  shuffle: boolean().nullable(),
@@ -61472,7 +61903,7 @@ var motionCustomize = {
61472
61903
  eventName: "onMotionChanged"
61473
61904
  },
61474
61905
  deviceTypes: [DeviceType.Camera],
61475
- apply: "replace",
61906
+ apply: "add",
61476
61907
  applyAuthority: {
61477
61908
  kind: "camera-sensors",
61478
61909
  timeoutField: "motionCooldownMs"
@@ -61544,7 +61975,7 @@ var MotionStatusSchema = object({
61544
61975
  * it, every provider seeded it at boot, and every session left it
61545
61976
  * `detected: true` after its detach.
61546
61977
  */
61547
- autoClearAfterMs: number().nullable()
61978
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61548
61979
  });
61549
61980
  /**
61550
61981
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61581,12 +62012,18 @@ var motionCapability = {
61581
62012
  methods: {
61582
62013
  /**
61583
62014
  * Pull the current motion state synchronously. Convenience shortcut
61584
- * for consumers that don't need the full status object; equivalent
61585
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61586
- * `getStatus` once the auto-injected status surface lands in every
61587
- * consumer.
62015
+ * for consumers that don't need the full status object. A composed
62016
+ * group answers from its slice (D944): unknown throws, it is not off.
61588
62017
  */
61589
62018
  isDetected: method(object({ deviceId: number() }), boolean()) },
62019
+ /**
62020
+ * A composed motion group serves `isDetected` from `detected` (D944).
62021
+ * Without this the cap stays uncomposable (`has-commands`).
62022
+ */
62023
+ composedMethods: { isDetected: {
62024
+ kind: "slice",
62025
+ field: "detected"
62026
+ } },
61590
62027
  events: {
61591
62028
  /**
61592
62029
  * Fires every time the runner transitions a camera between
@@ -61726,7 +62163,7 @@ var MotionZoneRegionSchema = object({
61726
62163
  * the grid region(s). */
61727
62164
  var MotionZoneStatusSchema = object({
61728
62165
  enabled: boolean(),
61729
- sensitivity: number(),
62166
+ sensitivity: number().meta(readingMeta("none")),
61730
62167
  /** Grid region(s). Today exactly one `grid` shape. */
61731
62168
  regions: array(MotionZoneRegionSchema),
61732
62169
  lastFetchedAt: number()
@@ -62096,7 +62533,7 @@ var NavigationStatusSchema = object({
62096
62533
  following: boolean(),
62097
62534
  flash: boolean(),
62098
62535
  lightMode: NavigationLightModeSchema,
62099
- lightLevel: number().min(40).max(100),
62536
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62100
62537
  /** Ms epoch when the slice was last updated. */
62101
62538
  lastChangedAt: number()
62102
62539
  });
@@ -62230,13 +62667,13 @@ var NetworkLinkStatusSchema = object({
62230
62667
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62231
62668
  * SKIP a null rather than coerce it.
62232
62669
  */
62233
- signalPercent: number().min(0).max(100).nullable(),
62670
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62234
62671
  /** Raw received signal strength in dBm, when the firmware reports one. */
62235
- rssiDbm: number().optional(),
62672
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62236
62673
  /** Network name of a wireless link, when the firmware reports it. */
62237
62674
  ssid: string().optional(),
62238
62675
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62239
- lastUpdated: number()
62676
+ lastUpdated: number().meta(readingMeta("none"))
62240
62677
  });
62241
62678
  var networkLinkCapability = {
62242
62679
  name: "network-link",
@@ -62621,7 +63058,7 @@ var NotifierStatusSchema = object({
62621
63058
  lastError: string().nullable(),
62622
63059
  /** Number of deliveries currently buffered server-side (rate-limited
62623
63060
  * notifiers may queue). 0 when the provider sends synchronously. */
62624
- queueDepth: number().int().nonnegative(),
63061
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62625
63062
  /** Per-feature capability matrix — authoritative source for the
62626
63063
  * compose-form field gating. */
62627
63064
  supports: NotifierSupportsSchema
@@ -62713,11 +63150,9 @@ onSent: { data: object({
62713
63150
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62714
63151
  * for: water flow, distance, weight, frequency, signal strength, …).
62715
63152
  *
62716
- * The `unit` and `precision` fields carry the live display metadata that
62717
- * the upstream source provides with each state push (HA
62718
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62719
- * The UI reads them directly from the slice — they are the single source
62720
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63153
+ * The quantity is not on this slice. It is chosen when the device is
63154
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63155
+ * that quantity's base unit. `null` is a refused sample (D393).
62721
63156
  *
62722
63157
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62723
63158
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62726,15 +63161,8 @@ onSent: { data: object({
62726
63161
  * numeric values land here.
62727
63162
  */
62728
63163
  var NumericSensorStatusSchema = object({
62729
- value: number(),
62730
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62731
- * Populated live from the upstream source on each state push.
62732
- * Absent for unitless sensors. */
62733
- unit: string().optional(),
62734
- /** Suggested decimal places for numeric display.
62735
- * Populated live from the upstream source when provided.
62736
- * Falls back to auto-formatting when absent. */
62737
- precision: number().int().min(0).max(10).optional(),
63164
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63165
+ value: number().nullable().meta(readingMeta("device")),
62738
63166
  /** Ms epoch when the slice was last updated. */
62739
63167
  lastFetchedAt: number()
62740
63168
  });
@@ -62758,11 +63186,11 @@ var numericSensorCapability = {
62758
63186
  */
62759
63187
  durability: "restored",
62760
63188
  /**
62761
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63189
+ * History (D850): recorded series. The unit is the device quantity's base,
63190
+ * resolved when the series is read — the cap has no single unit.
62762
63191
  */
62763
63192
  history: { fields: { value: {
62764
63193
  kind: "gauge",
62765
- unitFrom: "unit",
62766
63194
  minIntervalMs: 3e4
62767
63195
  } } },
62768
63196
  /** Clock fields: written, but excluded from the compare that decides
@@ -63054,23 +63482,26 @@ var PetFeederStatusSchema = object({
63054
63482
  /** Food currently in the bowl (grams). Null when the device has not
63055
63483
  * reported a reading yet. On dual-hopper models this is the combined
63056
63484
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63057
- foodLevel: number().nullable(),
63485
+ foodLevel: number().nullable().meta(readingMeta("none")),
63058
63486
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63059
63487
  * single-hopper models. */
63060
- food1: number().nullable(),
63488
+ food1: number().nullable().meta(readingMeta("none")),
63061
63489
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63062
63490
  * single-hopper models. */
63063
- food2: number().nullable(),
63491
+ food2: number().nullable().meta(readingMeta("none")),
63064
63492
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63065
63493
  * (`device_class: problem`, on = low). True when the bowl is empty /
63066
63494
  * below the feeder's low threshold. */
63067
63495
  lowFood: boolean(),
63068
63496
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63069
63497
  * device has no battery reading. */
63070
- batteryPower: number().min(0).max(100).nullable(),
63071
- /** Days of desiccant life remaining. Null when the model has no
63072
- * desiccant sensor. */
63073
- desiccantLeftDays: number().nullable(),
63498
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63499
+ /**
63500
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63501
+ * Providers convert from the days the feeder reports. Null when the
63502
+ * model has no desiccant sensor.
63503
+ */
63504
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63074
63505
  /** True while a feed is in progress. */
63075
63506
  feeding: boolean(),
63076
63507
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63081,7 +63512,7 @@ var PetFeederStatusSchema = object({
63081
63512
  * with `errorCode` for consumers that want the raw integer. */
63082
63513
  error: string().nullable(),
63083
63514
  /** Raw device error code (0 / null = no error). */
63084
- errorCode: number().nullable(),
63515
+ errorCode: number().nullable().meta(readingMeta("none")),
63085
63516
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63086
63517
  isDualHopper: boolean(),
63087
63518
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63091,7 +63522,7 @@ var PetFeederStatusSchema = object({
63091
63522
  /** Play a chime when dispensing. */
63092
63523
  feedSound: boolean(),
63093
63524
  /** Speaker / prompt volume level (device-scaled integer). */
63094
- volume: number(),
63525
+ volume: number().meta(readingMeta("none")),
63095
63526
  /** Ms epoch when the slice was last refreshed from the cloud. */
63096
63527
  lastFetchedAt: number()
63097
63528
  });
@@ -63210,7 +63641,7 @@ var petFeederCapability = {
63210
63641
  },
63211
63642
  desiccantLeftDays: {
63212
63643
  kind: "gauge",
63213
- unit: "days"
63644
+ unit: "s"
63214
63645
  }
63215
63646
  } }
63216
63647
  };
@@ -63611,36 +64042,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63611
64042
  */
63612
64043
  var PowerMeterStatusSchema = object({
63613
64044
  /** Instantaneous power draw in watts. */
63614
- watts: number().optional().meta({
63615
- unit: "W",
63616
- label: "Power"
63617
- }),
64045
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63618
64046
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63619
- kwhTotal: number().optional().meta({
63620
- unit: "kWh",
63621
- label: "Energy"
63622
- }),
64047
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63623
64048
  /** Voltage in volts. */
63624
- volts: number().optional(),
64049
+ volts: number().optional().meta(readingMeta("voltage")),
63625
64050
  /** Current in amperes. */
63626
- amps: number().optional(),
63627
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63628
- va: number().optional(),
64051
+ amps: number().optional().meta(readingMeta("current")),
64052
+ /** Apparent power in volt-amperes. */
64053
+ va: number().optional().meta(readingMeta("apparent-power")),
63629
64054
  /** Power factor (0..1) if reported. */
63630
- powerFactor: number().min(0).max(1).optional(),
64055
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63631
64056
  /** Ms epoch when the slice was last updated. */
63632
- lastFetchedAt: number(),
63633
- /** Live display unit of the single metric this slice carries (e.g. HA
63634
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63635
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63636
- * unit per slice is unambiguous. The UI prefers this over the role's
63637
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63638
- unit: string().optional(),
63639
- /** Suggested decimal places for numeric display.
63640
- * Populated live from the upstream source when provided (e.g. HA
63641
- * `attributes.suggested_display_precision`). Falls back to
63642
- * auto-formatting when absent. */
63643
- precision: number().int().min(0).max(10).optional()
64057
+ lastFetchedAt: number()
63644
64058
  });
63645
64059
  var powerMeterCapability = {
63646
64060
  name: "power-meter",
@@ -63704,12 +64118,7 @@ var powerMeterCapability = {
63704
64118
  } },
63705
64119
  /** Clock fields: written, but excluded from the compare that decides
63706
64120
  * whether persisting is worth a SQLite commit. */
63707
- volatileStateFields: ["lastFetchedAt"],
63708
- customize: {
63709
- version: 1,
63710
- advanced: "derive",
63711
- targets: []
63712
- }
64121
+ volatileStateFields: ["lastFetchedAt"]
63713
64122
  };
63714
64123
  /**
63715
64124
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63729,11 +64138,11 @@ var powerMeterCapability = {
63729
64138
  */
63730
64139
  var GpsLocationSchema = object({
63731
64140
  /** Latitude in decimal degrees, -90..90. */
63732
- latitude: number().min(-90).max(90),
64141
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63733
64142
  /** Longitude in decimal degrees, -180..180. */
63734
- longitude: number().min(-180).max(180),
64143
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63735
64144
  /** Reported accuracy in meters (lower = better). */
63736
- accuracyMeters: number().nonnegative()
64145
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63737
64146
  });
63738
64147
  var PresenceStatusSchema = object({
63739
64148
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63745,7 +64154,7 @@ var PresenceStatusSchema = object({
63745
64154
  gps: GpsLocationSchema.nullable(),
63746
64155
  /** Optional battery level of the tracking device (0..100). Null
63747
64156
  * when not reported. */
63748
- batteryPercent: number().min(0).max(100).nullable(),
64157
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63749
64158
  /** Ms epoch when the slice was last updated. */
63750
64159
  lastChangedAt: number()
63751
64160
  });
@@ -63795,19 +64204,10 @@ var presenceCapability = {
63795
64204
  * `sourceInfo`.
63796
64205
  */
63797
64206
  var PressureSensorStatusSchema = object({
63798
- /** Current pressure in hPa. */
63799
- hpa: number(),
64207
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64208
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63800
64209
  /** Ms epoch when the slice was last updated. */
63801
- lastFetchedAt: number(),
63802
- /** Live display unit from the upstream source (e.g. HA
63803
- * `attributes.unit_of_measurement`). The UI prefers this over the
63804
- * role's canonical unit. Absent → fall back to the canonical unit. */
63805
- unit: string().optional(),
63806
- /** Suggested decimal places for numeric display.
63807
- * Populated live from the upstream source when provided (e.g. HA
63808
- * `attributes.suggested_display_precision`). Falls back to
63809
- * auto-formatting when absent. */
63810
- precision: number().int().min(0).max(10).optional()
64210
+ lastFetchedAt: number()
63811
64211
  });
63812
64212
  var pressureSensorCapability = {
63813
64213
  name: "pressure-sensor",
@@ -64221,8 +64621,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64221
64621
  }), _void(), { kind: "mutation" });
64222
64622
  var PtzAutotrackSettingsSchema = object({
64223
64623
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64224
- stopDelaySeconds: number().int().min(0).max(300),
64225
- disappearDelaySeconds: number().int().min(0).max(300)
64624
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64625
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64226
64626
  });
64227
64627
  /**
64228
64628
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64376,7 +64776,7 @@ var RunningStateStatusSchema = object({
64376
64776
  /** null = unknown — never coerced to false (D393). */
64377
64777
  running: boolean().nullable(),
64378
64778
  /** Epoch ms the current boolean began; null while `running` is null. */
64379
- since: number().nullable(),
64779
+ since: number().nullable().meta(readingMeta("none")),
64380
64780
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64381
64781
  phase: string().nullable(),
64382
64782
  /** null until a reading has been published (D393). */
@@ -64391,7 +64791,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64391
64791
  /** Generic Customize target for the running boolean (D936). */
64392
64792
  var runningCustomize = {
64393
64793
  version: 1,
64394
- advanced: "derive",
64395
64794
  targets: [{
64396
64795
  id: "running",
64397
64796
  cardinality: "scalar",
@@ -64411,7 +64810,7 @@ var runningCustomize = {
64411
64810
  DeviceType.Thermostat,
64412
64811
  DeviceType.PetFeeder
64413
64812
  ],
64414
- apply: "add-or-replace",
64813
+ apply: "add",
64415
64814
  path: "running",
64416
64815
  sources: [
64417
64816
  {
@@ -65675,9 +66074,9 @@ var RecordTriggerSchema = _enum([
65675
66074
  */
65676
66075
  var RecordWindowSchema = object({
65677
66076
  trigger: RecordTriggerSchema,
65678
- day: number().int().min(0).max(6),
65679
- startMinute: number().int().min(0).max(1439),
65680
- endMinute: number().int().min(1).max(1440)
66077
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66078
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66079
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65681
66080
  });
65682
66081
  /** Status of one physical volume, as the camera itself describes it. */
65683
66082
  var OnboardStorageVolumeSchema = object({
@@ -65753,10 +66152,10 @@ var RecordingOnboardStatusSchema = object({
65753
66152
  /** Master "record to the card at all" switch. */
65754
66153
  enabled: boolean().nullable(),
65755
66154
  overwriteWhenFull: boolean().nullable(),
65756
- preRecordSec: number().nullable(),
65757
- postRecordSec: number().nullable(),
66155
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66156
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65758
66157
  /** Length of one recorded file, in minutes. */
65759
- segmentMinutes: number().nullable(),
66158
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65760
66159
  /** The primary track's weekly windows, flattened. */
65761
66160
  windows: array(RecordWindowSchema),
65762
66161
  /**
@@ -65772,7 +66171,7 @@ var RecordingOnboardStatusSchema = object({
65772
66171
  * provider reporting a non-zero count also reports the schedule as not
65773
66172
  * writable.
65774
66173
  */
65775
- unreadableWindows: number(),
66174
+ unreadableWindows: number().meta(readingMeta("count")),
65776
66175
  /**
65777
66176
  * The camera is scheduled to record and has NO usable storage.
65778
66177
  *
@@ -66617,7 +67016,7 @@ var CamStreamDescriptorSchema = object({
66617
67016
  url: string().optional(),
66618
67017
  codec: string().optional(),
66619
67018
  resolution: CamStreamResolutionSchema.optional(),
66620
- fps: number().positive().optional(),
67019
+ fps: number().positive().optional().meta(readingMeta("rate")),
66621
67020
  label: string().optional(),
66622
67021
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66623
67022
  deviceFeatures: array(string()).optional(),
@@ -66704,18 +67103,18 @@ var StreamProfileSchema = _enum([
66704
67103
  ]);
66705
67104
  /** The current encoding config of one stream profile. */
66706
67105
  var StreamProfileConfigSchema = object({
66707
- width: number(),
66708
- height: number(),
67106
+ width: number().meta(readingMeta("none")),
67107
+ height: number().meta(readingMeta("none")),
66709
67108
  codec: _enum(["h264", "h265"]),
66710
- framerate: number(),
66711
- bitrate: number(),
67109
+ framerate: number().meta(readingMeta("rate")),
67110
+ bitrate: number().meta(readingMeta("bitrate")),
66712
67111
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66713
67112
  encoderProfile: _enum([
66714
67113
  "high",
66715
67114
  "main",
66716
67115
  "baseline"
66717
67116
  ]).optional(),
66718
- gop: number().optional(),
67117
+ gop: number().optional().meta(readingMeta("none")),
66719
67118
  /**
66720
67119
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66721
67120
  *
@@ -66936,12 +67335,7 @@ var switchCapability = {
66936
67335
  label: "On",
66937
67336
  kind: "boolean"
66938
67337
  }
66939
- }] },
66940
- customize: {
66941
- version: 1,
66942
- advanced: "derive",
66943
- targets: []
66944
- }
67338
+ }] }
66945
67339
  };
66946
67340
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66947
67341
  function hasControlCharacter(value) {
@@ -67524,35 +67918,21 @@ var tamperCapability = {
67524
67918
  * entries with `device_class: temperature` and any future native
67525
67919
  * thermometer.
67526
67920
  *
67527
- * Unit normalisation: providers convert to Celsius before storing.
67528
- * The slice value is always Celsius so cross-cap aggregators
67529
- * (climate-control's `currentTemp`, energy analytics) can compose
67530
- * without per-source unit fixups. The canonical display unit (`°C`) is
67531
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67532
- * `sourceInfo`.
67921
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67922
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67533
67923
  *
67534
67924
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67535
67925
  * frozen feed (provider hung) distinct from a "temperature hasn't
67536
67926
  * changed" steady state.
67537
67927
  */
67538
67928
  var TemperatureSensorStatusSchema = object({
67539
- /** Current temperature in Celsius. */
67540
- celsius: number().meta({
67541
- unit: "°C",
67929
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67930
+ celsius: number().nullable().meta(readingMeta("temperature", {
67931
+ precision: 1,
67542
67932
  label: "Temperature"
67543
- }),
67933
+ })),
67544
67934
  /** Ms epoch when the slice was last updated (push or poll). */
67545
- lastFetchedAt: number(),
67546
- /** Live display unit from the upstream source (e.g. HA
67547
- * `attributes.unit_of_measurement`). The UI prefers this over the
67548
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67549
- * Absent → the UI falls back to the role's canonical unit. */
67550
- unit: string().optional(),
67551
- /** Suggested decimal places for numeric display.
67552
- * Populated live from the upstream source when provided (e.g. HA
67553
- * `attributes.suggested_display_precision`). Falls back to
67554
- * auto-formatting when absent. */
67555
- precision: number().int().min(0).max(10).optional()
67935
+ lastFetchedAt: number()
67556
67936
  });
67557
67937
  var temperatureSensorCapability = {
67558
67938
  name: "temperature-sensor",
@@ -67583,12 +67963,7 @@ var temperatureSensorCapability = {
67583
67963
  } } },
67584
67964
  /** Clock fields: written, but excluded from the compare that decides
67585
67965
  * whether persisting is worth a SQLite commit. */
67586
- volatileStateFields: ["lastFetchedAt"],
67587
- customize: {
67588
- version: 1,
67589
- advanced: "derive",
67590
- targets: []
67591
- }
67966
+ volatileStateFields: ["lastFetchedAt"]
67592
67967
  };
67593
67968
  /**
67594
67969
  * toast — system-scoped singleton capability that streams toast
@@ -67871,16 +68246,16 @@ var VacuumControlStatusSchema = object({
67871
68246
  ]),
67872
68247
  /** 0..100 battery percentage. Null when the device has no battery
67873
68248
  * reading. */
67874
- batteryLevel: number().min(0).max(100).nullable(),
68249
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67875
68250
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67876
68251
  * the vacuum has no speed control. */
67877
68252
  fanSpeed: string().nullable(),
67878
68253
  /** Speed tokens the hardware accepts — drives the UI selector. */
67879
68254
  availableFanSpeeds: array(string()),
67880
68255
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67881
- progressPercent: number().min(0).max(100).nullable(),
68256
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67882
68257
  /** Current error code (0 / null = no error). */
67883
- errorCode: number().nullable(),
68258
+ errorCode: number().nullable().meta(readingMeta("none")),
67884
68259
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67885
68260
  errorLabel: string().nullable(),
67886
68261
  /** Ms epoch when the slice was last updated. */
@@ -67958,7 +68333,7 @@ var ValveStatusSchema = object({
67958
68333
  ]),
67959
68334
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67960
68335
  * intermediate position surface. */
67961
- position: number().min(0).max(100).nullable(),
68336
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67962
68337
  /** Ms epoch when the slice was last updated. */
67963
68338
  lastChangedAt: number()
67964
68339
  });
@@ -68066,9 +68441,9 @@ var vibrationCapability = {
68066
68441
  */
68067
68442
  var WaterHeaterStatusSchema = object({
68068
68443
  /** Current measured temperature. Null when not reported. */
68069
- currentTemp: number().nullable(),
68444
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68070
68445
  /** Target temperature setpoint. Null when no setpoint surface. */
68071
- targetTemp: number().nullable(),
68446
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68072
68447
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68073
68448
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68074
68449
  * device reports an unknown state. */
@@ -68079,9 +68454,9 @@ var WaterHeaterStatusSchema = object({
68079
68454
  * has no away surface. */
68080
68455
  away: boolean().nullable(),
68081
68456
  /** HA `min_temp` attribute. Null when not reported. */
68082
- minTemp: number().nullable(),
68457
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68083
68458
  /** HA `max_temp` attribute. Null when not reported. */
68084
- maxTemp: number().nullable(),
68459
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68085
68460
  /** Ms epoch when the slice was last updated. */
68086
68461
  lastChangedAt: number()
68087
68462
  });
@@ -68166,25 +68541,18 @@ var waterHeaterCapability = {
68166
68541
  * Forecast deferred — current conditions only for v1.
68167
68542
  */
68168
68543
  var WeatherStatusSchema = object({
68169
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68170
- * when no condition has been reported yet. */
68544
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68171
68545
  condition: string().nullable(),
68172
- /** Current temperature in the reported unit. Null when not provided. */
68173
- temperature: number().nullable(),
68174
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68175
- temperatureUnit: string().nullable(),
68546
+ /** Current temperature in Celsius. Null when not provided or refused. */
68547
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68176
68548
  /** Relative humidity (0..100). Null when not provided. */
68177
- humidity: number().min(0).max(100).nullable(),
68178
- /** Barometric pressure in the reported unit. Null when not provided. */
68179
- pressure: number().nullable(),
68180
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68181
- pressureUnit: string().nullable(),
68182
- /** Wind speed in the reported unit. Null when not provided. */
68183
- windSpeed: number().nullable(),
68184
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68185
- windSpeedUnit: string().nullable(),
68549
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68550
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68551
+ pressure: number().nullable().meta(readingMeta("pressure")),
68552
+ /** Wind speed in metres per second. Null when not provided or refused. */
68553
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68186
68554
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68187
- windBearing: number().nullable(),
68555
+ windBearing: number().nullable().meta(readingMeta("angle")),
68188
68556
  /** Ms epoch when the slice was last updated. */
68189
68557
  lastFetchedAt: number()
68190
68558
  });
@@ -68212,12 +68580,12 @@ var weatherCapability = {
68212
68580
  */
68213
68581
  durability: "session",
68214
68582
  /**
68215
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68583
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68216
68584
  */
68217
68585
  history: { fields: {
68218
68586
  temperature: {
68219
68587
  kind: "gauge",
68220
- unitFrom: "temperatureUnit",
68588
+ unit: "°C",
68221
68589
  deadband: .2
68222
68590
  },
68223
68591
  humidity: {
@@ -68227,12 +68595,12 @@ var weatherCapability = {
68227
68595
  },
68228
68596
  pressure: {
68229
68597
  kind: "gauge",
68230
- unitFrom: "pressureUnit",
68598
+ unit: "hPa",
68231
68599
  deadband: .5
68232
68600
  },
68233
68601
  windSpeed: {
68234
68602
  kind: "gauge",
68235
- unitFrom: "windSpeedUnit",
68603
+ unit: "m/s",
68236
68604
  deadband: .5
68237
68605
  }
68238
68606
  } }
@@ -69463,6 +69831,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69463
69831
  featureProbe: featureProbeCapability,
69464
69832
  flood: floodCapability,
69465
69833
  gas: gasCapability,
69834
+ groupCommands: groupCommandsCapability,
69835
+ groupStatus: groupStatusCapability,
69466
69836
  heatingValve: heatingValveCapability,
69467
69837
  humidifier: humidifierCapability,
69468
69838
  humiditySensor: humiditySensorCapability,
@@ -70839,6 +71209,13 @@ Object.freeze({
70839
71209
  access: "view",
70840
71210
  role: "admin"
70841
71211
  },
71212
+ "addons.getPackageDocs": {
71213
+ capName: "addons",
71214
+ capScope: "system",
71215
+ addonId: null,
71216
+ access: "view",
71217
+ role: "admin"
71218
+ },
70842
71219
  "addons.getVersions": {
70843
71220
  capName: "addons",
70844
71221
  capScope: "system",
@@ -73947,6 +74324,20 @@ Object.freeze({
73947
74324
  access: "view",
73948
74325
  role: "viewer"
73949
74326
  },
74327
+ "groupCommands.getStatus": {
74328
+ capName: "group-commands",
74329
+ capScope: "device",
74330
+ addonId: null,
74331
+ access: "view",
74332
+ role: "viewer"
74333
+ },
74334
+ "groupStatus.getStatus": {
74335
+ capName: "group-status",
74336
+ capScope: "device",
74337
+ addonId: null,
74338
+ access: "view",
74339
+ role: "viewer"
74340
+ },
73950
74341
  "heatingValve.getStatus": {
73951
74342
  capName: "heating-valve",
73952
74343
  capScope: "device",
@@ -83769,6 +84160,37 @@ var SENSOR_CAP_VOCABULARY = {
83769
84160
  icon: "flame"
83770
84161
  }]
83771
84162
  },
84163
+ "group-commands": {
84164
+ scope: "device",
84165
+ noun: t("Group command", "Comando di gruppo")
84166
+ },
84167
+ "group-status": {
84168
+ scope: "device",
84169
+ noun: t("Group", "Gruppo"),
84170
+ state: [
84171
+ {
84172
+ kind: "numeric",
84173
+ field: "included",
84174
+ quantity: t("the members reporting", "i membri che rispondono"),
84175
+ label: t("Reporting", "Rispondono"),
84176
+ icon: "users"
84177
+ },
84178
+ {
84179
+ kind: "numeric",
84180
+ field: "excluded",
84181
+ quantity: t("the members left out", "i membri esclusi"),
84182
+ label: t("Excluded", "Esclusi"),
84183
+ icon: "users"
84184
+ },
84185
+ {
84186
+ kind: "numeric",
84187
+ field: "total",
84188
+ quantity: t("the member count", "il numero di membri"),
84189
+ label: t("Members", "Membri"),
84190
+ icon: "sigma"
84191
+ }
84192
+ ]
84193
+ },
83772
84194
  "humidity-sensor": {
83773
84195
  scope: "device",
83774
84196
  noun: t("Humidity", "Umidità"),
@@ -83802,7 +84224,6 @@ var SENSOR_CAP_VOCABULARY = {
83802
84224
  field: "value",
83803
84225
  quantity: t("the reading", "il valore"),
83804
84226
  label: t("Reading", "Valore"),
83805
- unitField: "unit",
83806
84227
  icon: "gauge"
83807
84228
  }]
83808
84229
  },
@@ -83938,7 +84359,7 @@ var SENSOR_CAP_VOCABULARY = {
83938
84359
  field: "temperature",
83939
84360
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83940
84361
  label: t("Temperature", "Temperatura"),
83941
- unitField: "temperatureUnit",
84362
+ unit: "°C",
83942
84363
  icon: "thermometer"
83943
84364
  },
83944
84365
  {
@@ -83954,7 +84375,7 @@ var SENSOR_CAP_VOCABULARY = {
83954
84375
  field: "windSpeed",
83955
84376
  quantity: t("the wind speed", "la velocità del vento"),
83956
84377
  label: t("Wind", "Vento"),
83957
- unitField: "windSpeedUnit",
84378
+ unit: "m/s",
83958
84379
  icon: "wind"
83959
84380
  },
83960
84381
  {
@@ -83962,7 +84383,7 @@ var SENSOR_CAP_VOCABULARY = {
83962
84383
  field: "pressure",
83963
84384
  quantity: t("the air pressure", "la pressione atmosferica"),
83964
84385
  label: t("Pressure", "Pressione"),
83965
- unitField: "pressureUnit",
84386
+ unit: "hPa",
83966
84387
  icon: "gauge"
83967
84388
  }
83968
84389
  ]
@@ -85301,6 +85722,16 @@ Object.freeze({
85301
85722
  form: "single",
85302
85723
  optional: false
85303
85724
  }],
85725
+ "groupCommands.getStatus": [{
85726
+ name: "deviceId",
85727
+ form: "single",
85728
+ optional: false
85729
+ }],
85730
+ "groupStatus.getStatus": [{
85731
+ name: "deviceId",
85732
+ form: "single",
85733
+ optional: false
85734
+ }],
85304
85735
  "heatingValve.getStatus": [{
85305
85736
  name: "deviceId",
85306
85737
  form: "single",
@@ -87285,6 +87716,8 @@ new Map(Object.entries({
87285
87716
  "detection-blind": "device-events",
87286
87717
  "motion-noisy": "device-events",
87287
87718
  "motion-calm": "device-events",
87719
+ "package-llm-silent": "device-events",
87720
+ "package-llm-answered": "device-events",
87288
87721
  "recording-stalled": "device-events",
87289
87722
  "recording-healthy": "device-events",
87290
87723
  "recording-degraded": "device-events",
@@ -87368,6 +87801,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87368
87801
  label: "Motion without a subject",
87369
87802
  kinds: ["motion-noisy", "motion-calm"]
87370
87803
  },
87804
+ {
87805
+ id: "package-llm",
87806
+ family: "device-events",
87807
+ label: "Package confirm",
87808
+ kinds: ["package-llm-silent", "package-llm-answered"]
87809
+ },
87371
87810
  {
87372
87811
  id: "recording",
87373
87812
  family: "device-events",
@@ -87549,6 +87988,8 @@ var DEVICE_EVENTS = [
87549
87988
  "detection-blind",
87550
87989
  "motion-noisy",
87551
87990
  "motion-calm",
87991
+ "package-llm-silent",
87992
+ "package-llm-answered",
87552
87993
  "recording-stalled",
87553
87994
  "recording-healthy",
87554
87995
  "recording-degraded",
@@ -87817,6 +88258,15 @@ new Map([
87817
88258
  snapshotLabel: "Cameras with constant motion now",
87818
88259
  snapshotSection: null
87819
88260
  },
88261
+ {
88262
+ id: "package-llm-confirm",
88263
+ family: "device-events",
88264
+ label: "Package confirm (not answering / answering again)",
88265
+ bad: ["package-llm-silent"],
88266
+ recovery: ["package-llm-answered"],
88267
+ snapshotLabel: "Cameras whose package confirm is not answering",
88268
+ snapshotSection: null
88269
+ },
87820
88270
  {
87821
88271
  id: "recording-health",
87822
88272
  family: "device-events",