@camstack/addon-provider-wyze 0.2.158 → 0.2.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -31,7 +31,7 @@ let net = require("net");
31
31
  net = __toESM(net, 1);
32
32
  let node_child_process = require("node:child_process");
33
33
  let node_tls = require("node:tls");
34
- //#region ../types/dist/event-category-DHXxB8t9.mjs
34
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
35
35
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
36
  EventCategory["SystemBoot"] = "system.boot";
37
37
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -357,6 +357,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
357
357
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
358
358
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
359
359
  /**
360
+ * A camera's package LLM confirm has failed closed three times in a row
361
+ * (D931, 2026-10-26). One edge per camera until
362
+ * {@link PipelinePackageLlmAnswered}.
363
+ */
364
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
365
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
366
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
367
+ /**
360
368
  * Per-camera pipeline config was mutated by the orchestrator
361
369
  * (a settings change via `setAgentInferenceDevices` /
362
370
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5466,7 +5474,7 @@ var ZodIssueCode = {
5466
5474
  var ZodFirstPartyTypeKind;
5467
5475
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5468
5476
  //#endregion
5469
- //#region ../types/dist/sleep-BErAxhul.mjs
5477
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5470
5478
  /**
5471
5479
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5472
5480
  * window to float samples (D455).
@@ -8020,6 +8028,99 @@ var RecordKindDeclarationSchema = object({
8020
8028
  }).strict();
8021
8029
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8022
8030
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8031
+ var READING_QUANTITIES = [
8032
+ "temperature",
8033
+ "pressure",
8034
+ "speed",
8035
+ "precipitation",
8036
+ "precipitation-rate",
8037
+ "length",
8038
+ "area",
8039
+ "illuminance",
8040
+ "irradiance",
8041
+ "power",
8042
+ "energy",
8043
+ "voltage",
8044
+ "current",
8045
+ "apparent-power",
8046
+ "humidity",
8047
+ "percentage",
8048
+ "concentration-volume",
8049
+ "concentration-mass",
8050
+ "concentration-ppb",
8051
+ "volume",
8052
+ "duration",
8053
+ "duration-ms",
8054
+ "information",
8055
+ "sound-level",
8056
+ "signal-level",
8057
+ "colour-temperature",
8058
+ "angle",
8059
+ "bitrate",
8060
+ "rate",
8061
+ "mass",
8062
+ "index",
8063
+ "ratio",
8064
+ "count",
8065
+ "none",
8066
+ "device"
8067
+ ];
8068
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8069
+ new Set(READING_QUANTITIES);
8070
+ /**
8071
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8072
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8073
+ */
8074
+ var QUANTITY_BASE = {
8075
+ temperature: "°C",
8076
+ pressure: "hPa",
8077
+ speed: "m/s",
8078
+ precipitation: "mm",
8079
+ "precipitation-rate": "mm/h",
8080
+ length: "m",
8081
+ area: "m²",
8082
+ illuminance: "lx",
8083
+ irradiance: "W/m²",
8084
+ power: "W",
8085
+ energy: "kWh",
8086
+ voltage: "V",
8087
+ current: "A",
8088
+ "apparent-power": "VA",
8089
+ humidity: "%",
8090
+ percentage: "%",
8091
+ "concentration-volume": "ppm",
8092
+ "concentration-mass": "µg/m³",
8093
+ "concentration-ppb": "ppb",
8094
+ volume: "L",
8095
+ duration: "s",
8096
+ "duration-ms": "ms",
8097
+ information: "B",
8098
+ "sound-level": "dBFS",
8099
+ "signal-level": "dBm",
8100
+ "colour-temperature": "mired",
8101
+ angle: "°",
8102
+ bitrate: "kbps",
8103
+ rate: "fps",
8104
+ mass: "kg",
8105
+ index: "none",
8106
+ ratio: "none",
8107
+ count: "none",
8108
+ none: "none",
8109
+ device: "device"
8110
+ };
8111
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8112
+ function baseUnitOf(quantity) {
8113
+ const symbol = QUANTITY_BASE[quantity];
8114
+ if (symbol === "none" || symbol === "device") return void 0;
8115
+ return symbol;
8116
+ }
8117
+ function readingMeta(quantity, extras) {
8118
+ return {
8119
+ quantity,
8120
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8121
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8122
+ };
8123
+ }
8023
8124
  /** Shared Zod schemas used across streaming capabilities. */
8024
8125
  var CamProfileSchema = _enum([
8025
8126
  "high",
@@ -8047,8 +8148,8 @@ var CamStreamKindSchema = _enum([
8047
8148
  "derived"
8048
8149
  ]);
8049
8150
  var CamStreamResolutionSchema = object({
8050
- width: number().int().positive(),
8051
- height: number().int().positive()
8151
+ width: number().int().positive().meta(readingMeta("none")),
8152
+ height: number().int().positive().meta(readingMeta("none"))
8052
8153
  });
8053
8154
  var CameraStreamSchema = object({
8054
8155
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8566,6 +8667,8 @@ var CAP_NAMES = Object.freeze([
8566
8667
  "filesystem-browse",
8567
8668
  "flood",
8568
8669
  "gas",
8670
+ "group-commands",
8671
+ "group-status",
8569
8672
  "heating-valve",
8570
8673
  "humidifier",
8571
8674
  "humidity-sensor",
@@ -8695,14 +8798,6 @@ Object.freeze({
8695
8798
  {
8696
8799
  "path": "aqi",
8697
8800
  "valueType": "number"
8698
- },
8699
- {
8700
- "path": "unit",
8701
- "valueType": "string"
8702
- },
8703
- {
8704
- "path": "precision",
8705
- "valueType": "number"
8706
8801
  }
8707
8802
  ],
8708
8803
  "alarm-panel": [
@@ -8738,20 +8833,10 @@ Object.freeze({
8738
8833
  "valueType": "boolean"
8739
8834
  }
8740
8835
  ],
8741
- "ambient-light-sensor": [
8742
- {
8743
- "path": "lux",
8744
- "valueType": "number"
8745
- },
8746
- {
8747
- "path": "unit",
8748
- "valueType": "string"
8749
- },
8750
- {
8751
- "path": "precision",
8752
- "valueType": "number"
8753
- }
8754
- ],
8836
+ "ambient-light-sensor": [{
8837
+ "path": "lux",
8838
+ "valueType": "number"
8839
+ }],
8755
8840
  "audio-metrics": [
8756
8841
  {
8757
8842
  "path": "ts",
@@ -8831,6 +8916,10 @@ Object.freeze({
8831
8916
  "path": "lastUpdated",
8832
8917
  "valueType": "number"
8833
8918
  },
8919
+ {
8920
+ "path": "volts",
8921
+ "valueType": "number"
8922
+ },
8834
8923
  {
8835
8924
  "path": "lastContactAt",
8836
8925
  "valueType": "number"
@@ -9073,10 +9162,6 @@ Object.freeze({
9073
9162
  "datetime"
9074
9163
  ]
9075
9164
  },
9076
- {
9077
- "path": "unit",
9078
- "valueType": "string"
9079
- },
9080
9165
  {
9081
9166
  "path": "min",
9082
9167
  "valueType": "number"
@@ -9088,19 +9173,6 @@ Object.freeze({
9088
9173
  {
9089
9174
  "path": "step",
9090
9175
  "valueType": "number"
9091
- },
9092
- {
9093
- "path": "precision",
9094
- "valueType": "number"
9095
- },
9096
- {
9097
- "path": "format",
9098
- "valueType": "enum",
9099
- "enumValues": [
9100
- "date",
9101
- "time",
9102
- "datetime"
9103
- ]
9104
9176
  }
9105
9177
  ],
9106
9178
  "cover": [
@@ -9341,13 +9413,8 @@ Object.freeze({
9341
9413
  "path": "value",
9342
9414
  "valueType": "string"
9343
9415
  }, {
9344
- "path": "format",
9345
- "valueType": "enum",
9346
- "enumValues": [
9347
- "date",
9348
- "time",
9349
- "datetime"
9350
- ]
9416
+ "path": "timestamp",
9417
+ "valueType": "boolean"
9351
9418
  }],
9352
9419
  "event-emitter": [
9353
9420
  {
@@ -9420,6 +9487,20 @@ Object.freeze({
9420
9487
  "path": "detected",
9421
9488
  "valueType": "boolean"
9422
9489
  }],
9490
+ "group-status": [
9491
+ {
9492
+ "path": "included",
9493
+ "valueType": "number"
9494
+ },
9495
+ {
9496
+ "path": "excluded",
9497
+ "valueType": "number"
9498
+ },
9499
+ {
9500
+ "path": "total",
9501
+ "valueType": "number"
9502
+ }
9503
+ ],
9423
9504
  "heating-valve": [{
9424
9505
  "path": "position",
9425
9506
  "valueType": "number"
@@ -9454,20 +9535,10 @@ Object.freeze({
9454
9535
  "valueType": "number"
9455
9536
  }
9456
9537
  ],
9457
- "humidity-sensor": [
9458
- {
9459
- "path": "percent",
9460
- "valueType": "number"
9461
- },
9462
- {
9463
- "path": "unit",
9464
- "valueType": "string"
9465
- },
9466
- {
9467
- "path": "precision",
9468
- "valueType": "number"
9469
- }
9470
- ],
9538
+ "humidity-sensor": [{
9539
+ "path": "percent",
9540
+ "valueType": "number"
9541
+ }],
9471
9542
  "image": [{
9472
9543
  "path": "url",
9473
9544
  "valueType": "string"
@@ -9806,20 +9877,10 @@ Object.freeze({
9806
9877
  "valueType": "boolean"
9807
9878
  }
9808
9879
  ],
9809
- "numeric-sensor": [
9810
- {
9811
- "path": "value",
9812
- "valueType": "number"
9813
- },
9814
- {
9815
- "path": "unit",
9816
- "valueType": "string"
9817
- },
9818
- {
9819
- "path": "precision",
9820
- "valueType": "number"
9821
- }
9822
- ],
9880
+ "numeric-sensor": [{
9881
+ "path": "value",
9882
+ "valueType": "number"
9883
+ }],
9823
9884
  "pet-feeder": [
9824
9885
  {
9825
9886
  "path": "foodLevel",
@@ -9911,14 +9972,6 @@ Object.freeze({
9911
9972
  {
9912
9973
  "path": "powerFactor",
9913
9974
  "valueType": "number"
9914
- },
9915
- {
9916
- "path": "unit",
9917
- "valueType": "string"
9918
- },
9919
- {
9920
- "path": "precision",
9921
- "valueType": "number"
9922
9975
  }
9923
9976
  ],
9924
9977
  "presence": [
@@ -9947,20 +10000,10 @@ Object.freeze({
9947
10000
  "valueType": "number"
9948
10001
  }
9949
10002
  ],
9950
- "pressure-sensor": [
9951
- {
9952
- "path": "hpa",
9953
- "valueType": "number"
9954
- },
9955
- {
9956
- "path": "unit",
9957
- "valueType": "string"
9958
- },
9959
- {
9960
- "path": "precision",
9961
- "valueType": "number"
9962
- }
9963
- ],
10003
+ "pressure-sensor": [{
10004
+ "path": "hpa",
10005
+ "valueType": "number"
10006
+ }],
9964
10007
  "privacy-mask": [{
9965
10008
  "path": "enabled",
9966
10009
  "valueType": "boolean"
@@ -10271,20 +10314,10 @@ Object.freeze({
10271
10314
  "path": "tampered",
10272
10315
  "valueType": "boolean"
10273
10316
  }],
10274
- "temperature-sensor": [
10275
- {
10276
- "path": "celsius",
10277
- "valueType": "number"
10278
- },
10279
- {
10280
- "path": "unit",
10281
- "valueType": "string"
10282
- },
10283
- {
10284
- "path": "precision",
10285
- "valueType": "number"
10286
- }
10287
- ],
10317
+ "temperature-sensor": [{
10318
+ "path": "celsius",
10319
+ "valueType": "number"
10320
+ }],
10288
10321
  "update": [
10289
10322
  {
10290
10323
  "path": "currentVersion",
@@ -10395,10 +10428,6 @@ Object.freeze({
10395
10428
  "path": "temperature",
10396
10429
  "valueType": "number"
10397
10430
  },
10398
- {
10399
- "path": "temperatureUnit",
10400
- "valueType": "string"
10401
- },
10402
10431
  {
10403
10432
  "path": "humidity",
10404
10433
  "valueType": "number"
@@ -10407,18 +10436,10 @@ Object.freeze({
10407
10436
  "path": "pressure",
10408
10437
  "valueType": "number"
10409
10438
  },
10410
- {
10411
- "path": "pressureUnit",
10412
- "valueType": "string"
10413
- },
10414
10439
  {
10415
10440
  "path": "windSpeed",
10416
10441
  "valueType": "number"
10417
10442
  },
10418
- {
10419
- "path": "windSpeedUnit",
10420
- "valueType": "string"
10421
- },
10422
10443
  {
10423
10444
  "path": "windBearing",
10424
10445
  "valueType": "number"
@@ -10835,7 +10856,7 @@ function errMsg(err) {
10835
10856
  return String(err);
10836
10857
  }
10837
10858
  //#endregion
10838
- //#region ../types/dist/composition-kEl9mevu.mjs
10859
+ //#region ../types/dist/composition-OhpuespA.mjs
10839
10860
  /**
10840
10861
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10841
10862
  *
@@ -11590,15 +11611,74 @@ function validateExpressionSource(src) {
11590
11611
  }
11591
11612
  return null;
11592
11613
  }
11593
- /** A composed device's stableId is this prefix plus the block id. */
11594
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11595
- /** Longest snippet a `code` source may carry (slice 4). */
11596
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11614
+ /**
11615
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11616
+ * id. Split out so helper schemas can name it without a cycle through
11617
+ * `composition.ts` (D659).
11618
+ */
11597
11619
  var CompositionSourceRefSchema = object({
11598
11620
  /** The owning addon; `stableId` is unique only within it. */
11599
11621
  addonId: string().min(1),
11600
11622
  stableId: string().min(1)
11601
11623
  });
11624
+ var GROUP_TEMPLATE_IDS = [
11625
+ "temperature",
11626
+ "humidity",
11627
+ "pressure",
11628
+ "illuminance",
11629
+ "power",
11630
+ "energy",
11631
+ "battery",
11632
+ "numeric",
11633
+ "motion",
11634
+ "contact",
11635
+ "leak",
11636
+ "smoke",
11637
+ "occupancy",
11638
+ "binary",
11639
+ "switch",
11640
+ "light"
11641
+ ];
11642
+ var NUMERIC_AGGREGATES = [
11643
+ "mean",
11644
+ "median",
11645
+ "min",
11646
+ "max",
11647
+ "sum"
11648
+ ];
11649
+ var BINARY_AGGREGATES = ["any", "all"];
11650
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11651
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11652
+ var AGGREGATE_REDUCERS = [
11653
+ "mean",
11654
+ "median",
11655
+ "min",
11656
+ "max",
11657
+ "sum",
11658
+ "any",
11659
+ "all"
11660
+ ];
11661
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11662
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11663
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11664
+ /** One member. The field is stored so occupancy can name motion or binary. */
11665
+ var HelperMemberSchema = object({
11666
+ source: CompositionSourceRefSchema,
11667
+ cap: string().min(1),
11668
+ fieldPath: string().min(1)
11669
+ });
11670
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11671
+ kind: literal("group"),
11672
+ templateId: GroupTemplateIdSchema,
11673
+ aggregate: AggregateReducerSchema,
11674
+ members: array(HelperMemberSchema).min(1).max(64),
11675
+ /** Actuator groups only. Readings leave this unset. */
11676
+ active: ActiveCriterionSchema.optional()
11677
+ })]);
11678
+ /** A composed device's stableId is this prefix plus the block id. */
11679
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11680
+ /** Longest snippet a `code` source may carry (slice 4). */
11681
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11602
11682
  /** One field of one capability of one source device. */
11603
11683
  var CompositionFieldReadSchema = object({
11604
11684
  source: CompositionSourceRefSchema,
@@ -11642,7 +11722,33 @@ var CompositionCodeSourceSchema = object({
11642
11722
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11643
11723
  CompositionFromSourceSchema,
11644
11724
  CompositionExpressionSourceSchema,
11645
- CompositionCodeSourceSchema
11725
+ CompositionCodeSourceSchema,
11726
+ object({
11727
+ kind: literal("aggregate"),
11728
+ reducer: _enum([
11729
+ "mean",
11730
+ "median",
11731
+ "min",
11732
+ "max",
11733
+ "sum",
11734
+ "any",
11735
+ "all"
11736
+ ]),
11737
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11738
+ /** `value` is the reading; the others are the member counts. */
11739
+ publish: _enum([
11740
+ "value",
11741
+ "included",
11742
+ "excluded",
11743
+ "total"
11744
+ ]),
11745
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11746
+ empty: _enum([
11747
+ "null",
11748
+ "omit",
11749
+ "offline"
11750
+ ])
11751
+ })
11646
11752
  ]);
11647
11753
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11648
11754
  kind: literal("forward"),
@@ -11688,7 +11794,13 @@ var CompositionSchema = object({
11688
11794
  kind: literal("existing"),
11689
11795
  device: CompositionSourceRefSchema
11690
11796
  })]),
11691
- features: array(CompositionFeatureSchema).min(1).max(16)
11797
+ features: array(CompositionFeatureSchema).min(1).max(16),
11798
+ /**
11799
+ * Present when the block is a helper template. Absent on a free
11800
+ * composition (the room example, device 4603). The editor reads this,
11801
+ * never the compiled features.
11802
+ */
11803
+ helper: HelperSpecSchema.optional()
11692
11804
  }).superRefine((composition, ctx) => {
11693
11805
  const seen = /* @__PURE__ */ new Set();
11694
11806
  composition.features.forEach((feature, index) => {
@@ -17916,7 +18028,11 @@ var CompositionProblemSchema = object({
17916
18028
  "item-field-reserved",
17917
18029
  "target-missing",
17918
18030
  "target-unreadable",
17919
- "not-replaceable"
18031
+ "not-replaceable",
18032
+ "native-capability-replace",
18033
+ "mixed-quantity",
18034
+ "helper-mismatch",
18035
+ "member-constraint"
17920
18036
  ]),
17921
18037
  path: string(),
17922
18038
  message: string()
@@ -17937,7 +18053,8 @@ var CompositionUnavailableModeSchema = _enum([
17937
18053
  "null",
17938
18054
  "offline",
17939
18055
  "unknown",
17940
- "release"
18056
+ "release",
18057
+ "omit"
17941
18058
  ]);
17942
18059
  /**
17943
18060
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17974,9 +18091,11 @@ var CompositionFieldPlanSchema = object({
17974
18091
  reads: array(CompositionFieldReadSchema)
17975
18092
  });
17976
18093
  /**
17977
- * What a feature does to its target (D663): `new` builds a capability on a new
17978
- * device; on an existing device it `add`s a capability the device lacks, or
17979
- * `replace`s named fields of one it has natively.
18094
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18095
+ * new device; on an existing device it `add`s a capability the device lacks.
18096
+ * Replacing a capability the device already serves is refused
18097
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18098
+ * wire for a claim the hub already holds.
17980
18099
  */
17981
18100
  var CompositionFeatureModeSchema = _enum([
17982
18101
  "new",
@@ -18149,6 +18268,275 @@ Object.freeze({
18149
18268
  result: ["number", "null"]
18150
18269
  }
18151
18270
  });
18271
+ /**
18272
+ * One catalog row per base reading a group helper can build (D944 §3).
18273
+ * Data, not a form default written into the block.
18274
+ */
18275
+ var NOW = {
18276
+ fieldPath: "lastFetchedAt",
18277
+ expr: "now"
18278
+ };
18279
+ var CHANGED = {
18280
+ fieldPath: "lastChangedAt",
18281
+ expr: "now"
18282
+ };
18283
+ function numeric(input) {
18284
+ return {
18285
+ ...input,
18286
+ family: "numeric",
18287
+ deviceType: DeviceType.Sensor,
18288
+ memberDeviceType: DeviceType.Sensor,
18289
+ commandCaps: [],
18290
+ baseUnit: baseUnitOf(input.quantity),
18291
+ aggregates: NUMERIC_AGGREGATES,
18292
+ accept: [{
18293
+ cap: input.outputCap,
18294
+ fieldPath: input.outputField
18295
+ }]
18296
+ };
18297
+ }
18298
+ function binary(input) {
18299
+ return {
18300
+ ...input,
18301
+ family: "binary",
18302
+ deviceType: DeviceType.Sensor,
18303
+ memberDeviceType: DeviceType.Sensor,
18304
+ commandCaps: [],
18305
+ quantity: "none",
18306
+ baseUnit: void 0,
18307
+ aggregates: BINARY_AGGREGATES,
18308
+ empty: "offline"
18309
+ };
18310
+ }
18311
+ var GROUP_TEMPLATES = [
18312
+ numeric({
18313
+ id: "temperature",
18314
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18315
+ role: DeviceRole.TemperatureSensor,
18316
+ outputCap: "temperature-sensor",
18317
+ outputField: "celsius",
18318
+ quantity: "temperature",
18319
+ defaultAggregate: "mean",
18320
+ empty: "null",
18321
+ companions: [NOW]
18322
+ }),
18323
+ numeric({
18324
+ id: "humidity",
18325
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18326
+ role: DeviceRole.HumiditySensor,
18327
+ outputCap: "humidity-sensor",
18328
+ outputField: "percent",
18329
+ quantity: "humidity",
18330
+ defaultAggregate: "mean",
18331
+ empty: "null",
18332
+ companions: [NOW]
18333
+ }),
18334
+ numeric({
18335
+ id: "pressure",
18336
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18337
+ role: DeviceRole.PressureSensor,
18338
+ outputCap: "pressure-sensor",
18339
+ outputField: "hpa",
18340
+ quantity: "pressure",
18341
+ defaultAggregate: "mean",
18342
+ empty: "null",
18343
+ companions: [NOW]
18344
+ }),
18345
+ numeric({
18346
+ id: "illuminance",
18347
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18348
+ role: DeviceRole.AmbientLightSensor,
18349
+ outputCap: "ambient-light-sensor",
18350
+ outputField: "lux",
18351
+ quantity: "illuminance",
18352
+ defaultAggregate: "mean",
18353
+ empty: "null",
18354
+ companions: [NOW]
18355
+ }),
18356
+ numeric({
18357
+ id: "power",
18358
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18359
+ role: DeviceRole.PowerSensor,
18360
+ outputCap: "power-meter",
18361
+ outputField: "watts",
18362
+ quantity: "power",
18363
+ defaultAggregate: "sum",
18364
+ empty: "omit",
18365
+ companions: [NOW]
18366
+ }),
18367
+ numeric({
18368
+ id: "energy",
18369
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18370
+ role: DeviceRole.EnergySensor,
18371
+ outputCap: "power-meter",
18372
+ outputField: "kwhTotal",
18373
+ quantity: "energy",
18374
+ defaultAggregate: "sum",
18375
+ empty: "omit",
18376
+ companions: [NOW]
18377
+ }),
18378
+ numeric({
18379
+ id: "battery",
18380
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18381
+ role: DeviceRole.BatterySensor,
18382
+ outputCap: "battery",
18383
+ outputField: "percentage",
18384
+ quantity: "percentage",
18385
+ defaultAggregate: "mean",
18386
+ empty: "null",
18387
+ companions: [
18388
+ {
18389
+ fieldPath: "charging",
18390
+ expr: "\"unknown\""
18391
+ },
18392
+ {
18393
+ fieldPath: "sleeping",
18394
+ expr: "false"
18395
+ },
18396
+ {
18397
+ fieldPath: "lastUpdated",
18398
+ expr: "now"
18399
+ }
18400
+ ]
18401
+ }),
18402
+ numeric({
18403
+ id: "numeric",
18404
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18405
+ role: DeviceRole.NumericSensor,
18406
+ outputCap: "numeric-sensor",
18407
+ outputField: "value",
18408
+ quantity: "device",
18409
+ defaultAggregate: "mean",
18410
+ empty: "null",
18411
+ companions: [NOW]
18412
+ }),
18413
+ binary({
18414
+ id: "motion",
18415
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18416
+ role: DeviceRole.MotionSensor,
18417
+ outputCap: "motion",
18418
+ outputField: "detected",
18419
+ defaultAggregate: "any",
18420
+ accept: [{
18421
+ cap: "motion",
18422
+ fieldPath: "detected"
18423
+ }],
18424
+ companions: [{
18425
+ fieldPath: "lastDetectedAt",
18426
+ expr: "null"
18427
+ }, {
18428
+ fieldPath: "autoClearAfterMs",
18429
+ expr: "null"
18430
+ }]
18431
+ }),
18432
+ binary({
18433
+ id: "contact",
18434
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18435
+ role: DeviceRole.ContactSensor,
18436
+ outputCap: "contact",
18437
+ outputField: "entryOpen",
18438
+ defaultAggregate: "any",
18439
+ accept: [{
18440
+ cap: "contact",
18441
+ fieldPath: "entryOpen"
18442
+ }],
18443
+ companions: [CHANGED]
18444
+ }),
18445
+ binary({
18446
+ id: "leak",
18447
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18448
+ role: DeviceRole.LeakSensor,
18449
+ outputCap: "flood",
18450
+ outputField: "flooded",
18451
+ defaultAggregate: "any",
18452
+ accept: [{
18453
+ cap: "flood",
18454
+ fieldPath: "flooded"
18455
+ }],
18456
+ companions: [CHANGED]
18457
+ }),
18458
+ binary({
18459
+ id: "smoke",
18460
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18461
+ role: DeviceRole.SmokeSensor,
18462
+ outputCap: "smoke",
18463
+ outputField: "detected",
18464
+ defaultAggregate: "any",
18465
+ accept: [{
18466
+ cap: "smoke",
18467
+ fieldPath: "detected"
18468
+ }],
18469
+ companions: [CHANGED]
18470
+ }),
18471
+ binary({
18472
+ id: "occupancy",
18473
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18474
+ role: DeviceRole.BinarySensor,
18475
+ outputCap: "binary",
18476
+ outputField: "on",
18477
+ defaultAggregate: "any",
18478
+ accept: [{
18479
+ cap: "motion",
18480
+ fieldPath: "detected"
18481
+ }, {
18482
+ cap: "binary",
18483
+ fieldPath: "on"
18484
+ }],
18485
+ companions: [CHANGED]
18486
+ }),
18487
+ binary({
18488
+ id: "binary",
18489
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18490
+ role: DeviceRole.BinarySensor,
18491
+ outputCap: "binary",
18492
+ outputField: "on",
18493
+ defaultAggregate: "any",
18494
+ accept: [{
18495
+ cap: "binary",
18496
+ fieldPath: "on"
18497
+ }],
18498
+ companions: [CHANGED]
18499
+ }),
18500
+ actuator({
18501
+ id: "switch",
18502
+ 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.",
18503
+ deviceType: DeviceType.Switch,
18504
+ outputCap: "switch",
18505
+ commandCaps: ["switch"]
18506
+ }),
18507
+ actuator({
18508
+ id: "light",
18509
+ 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.",
18510
+ deviceType: DeviceType.Light,
18511
+ outputCap: "switch",
18512
+ commandCaps: [
18513
+ "switch",
18514
+ "brightness",
18515
+ "color"
18516
+ ]
18517
+ })
18518
+ ];
18519
+ function actuator(input) {
18520
+ return {
18521
+ ...input,
18522
+ family: "actuator",
18523
+ role: void 0,
18524
+ memberDeviceType: input.deviceType,
18525
+ outputField: "on",
18526
+ quantity: "none",
18527
+ baseUnit: void 0,
18528
+ aggregates: BINARY_AGGREGATES,
18529
+ defaultAggregate: "any",
18530
+ empty: "offline",
18531
+ accept: [{
18532
+ cap: "switch",
18533
+ fieldPath: "on"
18534
+ }],
18535
+ companions: [CHANGED]
18536
+ };
18537
+ }
18538
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18539
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18152
18540
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18153
18541
  /**
18154
18542
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19862,12 +20250,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19862
20250
  * providers detect renames by matching `uniqueId` and patch `id`
19863
20251
  * in-place so dispatch keeps working.
19864
20252
  *
19865
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19866
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19867
- * state push and are the single source of truth. The `device_class` tag is
19868
- * an HA-specific input that is consumed at the discovery boundary
19869
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19870
- * stored after that point.
20253
+ * **Display unit and precision are NOT stored here.** They are a read-time
20254
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20255
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20256
+ * neither is repeated on each sample. The `device_class` tag is an
20257
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20258
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19871
20259
  *
19872
20260
  * The `raw` escape hatch carries provider-specific attributes that don't
19873
20261
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19881,6 +20269,14 @@ var SourceInfoSchema = object({
19881
20269
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19882
20270
  * Used to detect rename when `id` changes. */
19883
20271
  uniqueId: string().optional(),
20272
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20273
+ quantity: ReadingQuantitySchema.optional(),
20274
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20275
+ dateTimeShape: _enum([
20276
+ "date",
20277
+ "time",
20278
+ "datetime"
20279
+ ]).optional(),
19884
20280
  /** Provider-specific extras that don't fit the structured slots.
19885
20281
  * Whitelist what goes here per provider — do NOT dump entire HA
19886
20282
  * attribute blobs (would explode the meta JSON column). */
@@ -41002,7 +41398,8 @@ method(object({
41002
41398
  "enum"
41003
41399
  ]),
41004
41400
  enumValues: array(string()).optional(),
41005
- item: boolean().optional()
41401
+ item: boolean().optional(),
41402
+ quantity: ReadingQuantitySchema.optional()
41006
41403
  })).readonly(),
41007
41404
  itemArray: object({
41008
41405
  path: string(),
@@ -44846,8 +45243,8 @@ var MaskPolygonShapeSchema = object({
44846
45243
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44847
45244
  var MaskGridShapeSchema = object({
44848
45245
  kind: literal("grid"),
44849
- gridWidth: number(),
44850
- gridHeight: number(),
45246
+ gridWidth: number().meta(readingMeta("none")),
45247
+ gridHeight: number().meta(readingMeta("none")),
44851
45248
  cells: array(boolean())
44852
45249
  });
44853
45250
  discriminatedUnion("kind", [
@@ -44959,6 +45356,8 @@ var NcSystemEventKindSchema = _enum([
44959
45356
  "detection-blind",
44960
45357
  "motion-noisy",
44961
45358
  "motion-calm",
45359
+ "package-llm-silent",
45360
+ "package-llm-answered",
44962
45361
  "recording-stalled",
44963
45362
  "recording-healthy",
44964
45363
  "recording-degraded",
@@ -56154,6 +56553,28 @@ var PackageVersionInfoSchema = object({
56154
56553
  deprecated: string().optional(),
56155
56554
  distTags: array(string()).readonly()
56156
56555
  });
56556
+ /**
56557
+ * One published document. `ok` is markdown from the registry. `missing` means
56558
+ * the registry answered and the file was not there. `error` is a registry or
56559
+ * tarball failure — never an empty string standing in for either.
56560
+ */
56561
+ var PackageDocBodySchema = discriminatedUnion("status", [
56562
+ object({
56563
+ status: literal("ok"),
56564
+ markdown: string()
56565
+ }),
56566
+ object({ status: literal("missing") }),
56567
+ object({
56568
+ status: literal("error"),
56569
+ message: string()
56570
+ })
56571
+ ]);
56572
+ var PackageDocsSchema = object({
56573
+ name: string(),
56574
+ version: string(),
56575
+ readme: PackageDocBodySchema,
56576
+ changelog: PackageDocBodySchema
56577
+ });
56157
56578
  var SearchResultSchema = object({
56158
56579
  name: string(),
56159
56580
  version: string(),
@@ -56372,7 +56793,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56372
56793
  }), object({ success: literal(true) }), {
56373
56794
  kind: "mutation",
56374
56795
  auth: "admin"
56375
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56796
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56797
+ name: string().min(1),
56798
+ version: string().min(1).optional()
56799
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56376
56800
  kind: "mutation",
56377
56801
  auth: "admin"
56378
56802
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56431,27 +56855,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56431
56855
  */
56432
56856
  var AirQualitySensorStatusSchema = object({
56433
56857
  /** Carbon dioxide concentration in ppm. */
56434
- co2Ppm: number().min(0).optional(),
56858
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56435
56859
  /** Total volatile organic compounds in ppb. */
56436
- vocPpb: number().min(0).optional(),
56860
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56437
56861
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56438
- pm25: number().min(0).optional(),
56862
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56439
56863
  /** Particulate matter ≤ 10 μm in µg/m³. */
56440
- pm10: number().min(0).optional(),
56864
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56441
56865
  /** Composite AQI value (typically 0..500). */
56442
- aqi: number().optional(),
56866
+ aqi: number().optional().meta(readingMeta("index")),
56443
56867
  /** Ms epoch when the slice was last updated. */
56444
- lastFetchedAt: number(),
56445
- /** Live display unit of the single metric this slice carries (e.g. HA
56446
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56447
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56448
- * per slice is unambiguous. */
56449
- unit: string().optional(),
56450
- /** Suggested decimal places for numeric display.
56451
- * Populated live from the upstream source when provided (e.g. HA
56452
- * `attributes.suggested_display_precision`). Falls back to
56453
- * auto-formatting when absent. */
56454
- precision: number().int().min(0).max(10).optional()
56868
+ lastFetchedAt: number()
56455
56869
  });
56456
56870
  var airQualitySensorCapability = {
56457
56871
  name: "air-quality-sensor",
@@ -56488,12 +56902,12 @@ var airQualitySensorCapability = {
56488
56902
  },
56489
56903
  pm25: {
56490
56904
  kind: "gauge",
56491
- unit: "ug/m3",
56905
+ unit: "µg/m³",
56492
56906
  deadband: 1
56493
56907
  },
56494
56908
  pm10: {
56495
56909
  kind: "gauge",
56496
- unit: "ug/m3",
56910
+ unit: "µg/m³",
56497
56911
  deadband: 1
56498
56912
  },
56499
56913
  aqi: {
@@ -56511,22 +56925,13 @@ var airQualitySensorCapability = {
56511
56925
  * entries with `device_class: illuminance`.
56512
56926
  */
56513
56927
  var AmbientLightSensorStatusSchema = object({
56514
- /** Current illuminance in lux (lx). */
56515
- lux: number().min(0).meta({
56516
- unit: "lx",
56928
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56929
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56930
+ precision: 0,
56517
56931
  label: "Illuminance"
56518
- }),
56932
+ })),
56519
56933
  /** Ms epoch when the slice was last updated. */
56520
- lastFetchedAt: number(),
56521
- /** Live display unit from the upstream source (e.g. HA
56522
- * `attributes.unit_of_measurement`). The UI prefers this over the
56523
- * role's canonical unit. Absent → fall back to the canonical unit. */
56524
- unit: string().optional(),
56525
- /** Suggested decimal places for numeric display.
56526
- * Populated live from the upstream source when provided (e.g. HA
56527
- * `attributes.suggested_display_precision`). Falls back to
56528
- * auto-formatting when absent. */
56529
- precision: number().int().min(0).max(10).optional()
56934
+ lastFetchedAt: number()
56530
56935
  });
56531
56936
  var ambientLightSensorCapability = {
56532
56937
  name: "ambient-light-sensor",
@@ -56558,12 +56963,7 @@ var ambientLightSensorCapability = {
56558
56963
  } } },
56559
56964
  /** Clock fields: written, but excluded from the compare that decides
56560
56965
  * whether persisting is worth a SQLite commit. */
56561
- volatileStateFields: ["lastFetchedAt"],
56562
- customize: {
56563
- version: 1,
56564
- advanced: "derive",
56565
- targets: []
56566
- }
56966
+ volatileStateFields: ["lastFetchedAt"]
56567
56967
  };
56568
56968
  /**
56569
56969
  * Per-class audio metrics aggregated over a sliding window.
@@ -56790,10 +57190,7 @@ var BatteryStatusSchema = object({
56790
57190
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56791
57191
  * declines to band a non-finite reading, which is the correct shape.
56792
57192
  */
56793
- percentage: number().min(0).max(100).nullable().meta({
56794
- unit: "%",
56795
- label: "Charge"
56796
- }),
57193
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56797
57194
  /**
56798
57195
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56799
57196
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56820,7 +57217,14 @@ var BatteryStatusSchema = object({
56820
57217
  */
56821
57218
  sleeping: boolean().meta({ label: "Sleeping" }),
56822
57219
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56823
- lastUpdated: number(),
57220
+ lastUpdated: number().meta(readingMeta("none")),
57221
+ /**
57222
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57223
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57224
+ * that only report charge. A second reading on this cap, not a second
57225
+ * `numeric-sensor` on the same device.
57226
+ */
57227
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56824
57228
  /**
56825
57229
  * Ms epoch of the last time the device PROVED it was reachable — a
56826
57230
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56872,7 +57276,6 @@ var BATTERY_DEVICE_TYPES = [
56872
57276
  /** Generic Customize target for the percentage reading (D936). */
56873
57277
  var batteryCustomize = {
56874
57278
  version: 1,
56875
- advanced: "derive",
56876
57279
  targets: [{
56877
57280
  id: "percentage",
56878
57281
  cardinality: "scalar",
@@ -56881,7 +57284,7 @@ var batteryCustomize = {
56881
57284
  stateField: "percentage"
56882
57285
  },
56883
57286
  deviceTypes: BATTERY_DEVICE_TYPES,
56884
- apply: "add-or-replace",
57287
+ apply: "add",
56885
57288
  path: "percentage",
56886
57289
  sources: [{
56887
57290
  kind: "field",
@@ -57002,10 +57405,16 @@ onStatusChanged: { data: object({
57002
57405
  /**
57003
57406
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57004
57407
  */
57005
- history: { fields: { percentage: {
57006
- kind: "gauge",
57007
- unit: "%"
57008
- } } },
57408
+ history: { fields: {
57409
+ percentage: {
57410
+ kind: "gauge",
57411
+ unit: "%"
57412
+ },
57413
+ volts: {
57414
+ kind: "gauge",
57415
+ unit: "V"
57416
+ }
57417
+ } },
57009
57418
  customize: batteryCustomize,
57010
57419
  /** Clock fields: written, but excluded from the compare that decides
57011
57420
  * whether persisting is worth a SQLite commit. */
@@ -57052,12 +57461,7 @@ var binaryCapability = {
57052
57461
  history: { fields: { on: { kind: "boolean" } } },
57053
57462
  /** Clock fields: written, but excluded from the compare that decides
57054
57463
  * whether persisting is worth a SQLite commit. */
57055
- volatileStateFields: ["lastChangedAt"],
57056
- customize: {
57057
- version: 1,
57058
- advanced: "derive",
57059
- targets: []
57060
- }
57464
+ volatileStateFields: ["lastChangedAt"]
57061
57465
  };
57062
57466
  /**
57063
57467
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57073,7 +57477,7 @@ var binaryCapability = {
57073
57477
  */
57074
57478
  var BrightnessStatusSchema = object({
57075
57479
  /** Current level as 0..100 inclusive. Firmware-reported. */
57076
- percentage: number().min(0).max(100),
57480
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57077
57481
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57078
57482
  lastChangedAt: number()
57079
57483
  });
@@ -57527,21 +57931,21 @@ var ClimateControlStatusSchema = object({
57527
57931
  /** Single setpoint in Celsius. Used by single-target modes
57528
57932
  * (heat / cool / dry). Null when the device is in a range mode
57529
57933
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57530
- target: number().nullable(),
57934
+ target: number().nullable().meta(readingMeta("temperature")),
57531
57935
  /** Upper bound of the dual setpoint in Celsius. Populated only
57532
57936
  * in `heat_cool` mode. */
57533
- targetHigh: number().nullable(),
57937
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57534
57938
  /** Lower bound of the dual setpoint in Celsius. Populated only
57535
57939
  * in `heat_cool` mode. */
57536
- targetLow: number().nullable(),
57940
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57537
57941
  /** Target relative humidity (0..100). Null when the device has no
57538
57942
  * humidity control. */
57539
- targetHumidity: number().min(0).max(100).nullable(),
57943
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57540
57944
  /** Current measured temperature in Celsius. Read-only — pushed by
57541
57945
  * the device's internal sensor. */
57542
- currentTemp: number().nullable(),
57946
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57543
57947
  /** Current measured relative humidity (0..100). Read-only. */
57544
- currentHumidity: number().min(0).max(100).nullable(),
57948
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57545
57949
  /** Vertical louver swing on/off. Null when the device has no vertical
57546
57950
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57547
57951
  swingVertical: boolean().nullable(),
@@ -57549,11 +57953,11 @@ var ClimateControlStatusSchema = object({
57549
57953
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57550
57954
  swingHorizontal: boolean().nullable(),
57551
57955
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57552
- minTemp: number().nullable().optional(),
57956
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57553
57957
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57554
- maxTemp: number().nullable().optional(),
57958
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57555
57959
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57556
- tempStep: number().positive().nullable().optional(),
57960
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57557
57961
  /** Ms epoch when the slice was last updated (push or command). */
57558
57962
  lastFetchedAt: number()
57559
57963
  });
@@ -57695,17 +58099,17 @@ var climateControlCapability = {
57695
58099
  * modes to expose in the picker without a slice fetch.
57696
58100
  */
57697
58101
  var RgbTripletSchema = object({
57698
- r: number().int().min(0).max(255),
57699
- g: number().int().min(0).max(255),
57700
- b: number().int().min(0).max(255)
58102
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58103
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58104
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57701
58105
  });
57702
58106
  var HsvTripletSchema = object({
57703
58107
  /** Hue in degrees, 0..360. */
57704
- h: number().min(0).max(360),
58108
+ h: number().min(0).max(360).meta(readingMeta("none")),
57705
58109
  /** Saturation as 0..100 inclusive. */
57706
- s: number().min(0).max(100),
58110
+ s: number().min(0).max(100).meta(readingMeta("none")),
57707
58111
  /** Value/brightness as 0..100 inclusive. */
57708
- v: number().min(0).max(100)
58112
+ v: number().min(0).max(100).meta(readingMeta("none"))
57709
58113
  });
57710
58114
  var ColorInputSchema = discriminatedUnion("mode", [
57711
58115
  object({
@@ -57734,7 +58138,7 @@ var ColorStatusSchema = object({
57734
58138
  /** Populated when `mode === 'hsv'`. */
57735
58139
  hsv: HsvTripletSchema.optional(),
57736
58140
  /** Populated when `mode === 'mired'`. */
57737
- mireds: number().int().optional(),
58141
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57738
58142
  /** Ms epoch of the last operator-driven change. */
57739
58143
  lastChangedAt: number()
57740
58144
  });
@@ -57937,18 +58341,12 @@ _enum([
57937
58341
  "empty",
57938
58342
  "full"
57939
58343
  ]);
57940
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57941
- * each. */
57942
- var ConsumableRemainingUnitSchema = _enum([
57943
- "days",
57944
- "hours",
57945
- "cycles"
57946
- ]);
57947
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57948
- * days). `value` null = unknown (D393). */
58344
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58345
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58346
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57949
58347
  var ConsumableRemainingSchema = object({
57950
- value: number().min(0).nullable(),
57951
- unit: ConsumableRemainingUnitSchema
58348
+ value: number().min(0).nullable().meta(readingMeta("device")),
58349
+ kind: ConsumableRemainingKindSchema
57952
58350
  });
57953
58351
  /** A single consumable item. Either a continuous `level` (remaining
57954
58352
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57961,7 +58359,7 @@ var ConsumableItemSchema = object({
57961
58359
  /** Display name. */
57962
58360
  label: string().min(1),
57963
58361
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57964
- level: number().min(0).max(100).nullable(),
58362
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57965
58363
  /** The provider's discrete verdict when known (binary mode). */
57966
58364
  status: ConsumableStatusSchema.nullable(),
57967
58365
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58043,8 +58441,8 @@ var consumablesCustomize = {
58043
58441
  transforms: [{ kind: "identity" }]
58044
58442
  }],
58045
58443
  literals: [{
58046
- path: "remaining.unit",
58047
- values: [...ConsumableRemainingUnitSchema.options]
58444
+ path: "remaining.kind",
58445
+ values: [...ConsumableRemainingKindSchema.options]
58048
58446
  }]
58049
58447
  },
58050
58448
  {
@@ -58173,7 +58571,6 @@ reset: method(object({
58173
58571
  },
58174
58572
  "remaining.value": {
58175
58573
  kind: "gauge",
58176
- unitFrom: "remaining.unit",
58177
58574
  deadband: 1,
58178
58575
  displayName: "Remaining"
58179
58576
  }
@@ -58243,10 +58640,9 @@ var contactCapability = {
58243
58640
  * (empty array for the other kinds). It lives in the slice because
58244
58641
  * HA `select.*` entities may update their options at runtime.
58245
58642
  *
58246
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58247
- * `kind === 'numeric'` and are absent (undefined) for all other
58248
- * kinds. They are populated live from HA attributes on each state
58249
- * push — consistent with `options` and the sensor-unit approach.
58643
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58644
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58645
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58250
58646
  */
58251
58647
  var ControlStatusSchema = object({
58252
58648
  kind: _enum([
@@ -58258,39 +58654,17 @@ var ControlStatusSchema = object({
58258
58654
  /** Current value. Numeric kind → number; select/text/datetime → string.
58259
58655
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58260
58656
  * or time-only — HA accepts all three forms). */
58261
- value: union([number(), string()]),
58657
+ value: union([number().meta(readingMeta("device")), string()]),
58262
58658
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58263
58659
  options: array(string()),
58264
58660
  /** Ms epoch when the slice was last updated. */
58265
58661
  lastChangedAt: number(),
58266
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58267
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58268
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58269
- unit: string().optional(),
58270
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58271
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58272
- min: number().optional(),
58273
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58274
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58275
- max: number().optional(),
58276
- /** Step increment. Populated live from HA `attributes.step`.
58277
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58278
- step: number().optional(),
58279
- /** Suggested decimal places for numeric display. Populated live from HA
58280
- * `attributes.suggested_display_precision`, or derived from `step` when the
58281
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58282
- * other kinds. Falls back to auto-formatting when absent. */
58283
- precision: number().int().min(0).max(10).optional(),
58284
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58285
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58286
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58287
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58288
- * `has_date`/`has_time` attributes on each push. */
58289
- format: _enum([
58290
- "date",
58291
- "time",
58292
- "datetime"
58293
- ]).optional()
58662
+ /** Minimum allowed value, in the device quantity's base unit. */
58663
+ min: number().optional().meta(readingMeta("device")),
58664
+ /** Maximum allowed value, in the device quantity's base unit. */
58665
+ max: number().optional().meta(readingMeta("device")),
58666
+ /** Step increment, in the device quantity's base unit. */
58667
+ step: number().optional().meta(readingMeta("device"))
58294
58668
  });
58295
58669
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58296
58670
  object({
@@ -58356,9 +58730,9 @@ var CoverStatusSchema = object({
58356
58730
  ]),
58357
58731
  /** 0 = fully closed, 100 = fully open. Null when the device has
58358
58732
  * no intermediate position surface. */
58359
- position: number().min(0).max(100).nullable(),
58733
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58360
58734
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58361
- tiltPosition: number().min(0).max(100).nullable(),
58735
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58362
58736
  /** Ms epoch when the slice was last updated. */
58363
58737
  lastChangedAt: number()
58364
58738
  });
@@ -58461,9 +58835,9 @@ var NormalizedRangeSchema$1 = object({
58461
58835
  var DayNightStatusSchema = object({
58462
58836
  mode: DayNightModeSchema,
58463
58837
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58464
- sensitivity: number().optional(),
58838
+ sensitivity: number().optional().meta(readingMeta("none")),
58465
58839
  /** Delay before the IR-cut filter flips, in seconds. */
58466
- switchDelaySec: number().optional(),
58840
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58467
58841
  lastFetchedAt: number()
58468
58842
  });
58469
58843
  /**
@@ -58681,11 +59055,11 @@ var DishwasherStatusSchema = object({
58681
59055
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58682
59056
  availablePrograms: array(string()),
58683
59057
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58684
- remainingSeconds: number().int().nonnegative().nullable(),
59058
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58685
59059
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58686
- delayedStartSeconds: number().int().nonnegative().nullable(),
59060
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58687
59061
  /** Cycle completion 0..100, or null when not applicable. */
58688
- progressPercent: number().min(0).max(100).nullable(),
59062
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58689
59063
  /** True when the door is reported open; null when the door state is not known. */
58690
59064
  doorOpen: boolean().nullable(),
58691
59065
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58709,7 +59083,7 @@ var DishwasherStatusSchema = object({
58709
59083
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58710
59084
  machineCareDue: boolean().nullable(),
58711
59085
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58712
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59086
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58713
59087
  /** Option states: null when the appliance does not expose the option or has not said. */
58714
59088
  halfLoad: boolean().nullable(),
58715
59089
  hygienePlus: boolean().nullable(),
@@ -58724,7 +59098,7 @@ var DishwasherStatusSchema = object({
58724
59098
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58725
59099
  activeEvents: array(string()),
58726
59100
  /** Numeric error code when the appliance reports one; null otherwise. */
58727
- errorCode: number().nullable(),
59101
+ errorCode: number().nullable().meta(readingMeta("none")),
58728
59102
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58729
59103
  errorLabel: string().nullable(),
58730
59104
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58955,7 +59329,7 @@ var DoorbellStatusSchema = object({
58955
59329
  /** Ms epoch of the last press. null = never observed since this provider started. */
58956
59330
  lastPressedAt: number().nullable(),
58957
59331
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58958
- pressCountSinceStart: number()
59332
+ pressCountSinceStart: number().meta(readingMeta("count"))
58959
59333
  });
58960
59334
  var DoorbellPressEventSchema = object({
58961
59335
  deviceId: number(),
@@ -59020,17 +59394,7 @@ onPressed: {
59020
59394
  history: "none",
59021
59395
  customize: doorbellCustomize
59022
59396
  };
59023
- /**
59024
- * Enum-state sensor — a string value picked from a finite option set.
59025
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59026
- * states, weather conditions, contact-source channels, mode strings).
59027
- * The option set is stable across observations; it is persisted in the
59028
- * device config blob (via the control slice at adoption) and is NOT
59029
- * stored in `sourceInfo`.
59030
- */
59031
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59032
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59033
- var EnumSensorDateTimeFormatSchema = _enum([
59397
+ _enum([
59034
59398
  "date",
59035
59399
  "time",
59036
59400
  "datetime"
@@ -59038,12 +59402,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59038
59402
  var EnumSensorStatusSchema = object({
59039
59403
  value: string(),
59040
59404
  /**
59041
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59042
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59043
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59044
- * a time-only sensor → `'time'`.
59405
+ * Set when `value` is a timestamp rather than an enum state, so history
59406
+ * does not learn ISO strings as states. The display shape (date / time /
59407
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59045
59408
  */
59046
- format: EnumSensorDateTimeFormatSchema.optional(),
59409
+ timestamp: boolean().optional(),
59047
59410
  /** Ms epoch when the slice was last updated. */
59048
59411
  lastFetchedAt: number()
59049
59412
  });
@@ -59070,11 +59433,11 @@ var enumSensorCapability = {
59070
59433
  * History (D850): the value as an `enum` series (an appliance's program or
59071
59434
  * operation state, an HVAC action). The option set is per device, so the
59072
59435
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59073
- * `format` carries a timestamp, not a state, and is not recorded.
59436
+ * `timestamp` carries a clock, not a state, and is not recorded.
59074
59437
  */
59075
59438
  history: { fields: { value: {
59076
59439
  kind: "enum",
59077
- skipWhenSet: "format"
59440
+ skipWhenSet: "timestamp"
59078
59441
  } } },
59079
59442
  /** Clock fields: written, but excluded from the compare that decides
59080
59443
  * whether persisting is worth a SQLite commit. */
@@ -59094,13 +59457,13 @@ var EventFireSchema = object({
59094
59457
  deviceId: number(),
59095
59458
  eventType: string(),
59096
59459
  data: record(string(), unknown()).nullable(),
59097
- timestamp: number(),
59098
- seq: number()
59460
+ timestamp: number().meta(readingMeta("none")),
59461
+ seq: number().meta(readingMeta("count"))
59099
59462
  });
59100
59463
  var EventEmitterStatusSchema = object({
59101
59464
  eventTypes: array(string()),
59102
59465
  lastEvent: EventFireSchema.nullable(),
59103
- eventCountSinceStart: number()
59466
+ eventCountSinceStart: number().meta(readingMeta("count"))
59104
59467
  });
59105
59468
  var eventEmitterCapability = {
59106
59469
  name: "event-emitter",
@@ -59513,10 +59876,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59513
59876
  var FanControlStatusSchema = object({
59514
59877
  /** Active speed as 0..100 inclusive. Null when the device has no
59515
59878
  * speed surface (single-speed fan). */
59516
- percentage: number().min(0).max(100).nullable(),
59879
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59517
59880
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59518
59881
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59519
- percentageStep: number().positive().optional(),
59882
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59520
59883
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59521
59884
  * — empty string when no preset is active or supported. */
59522
59885
  preset: string(),
@@ -59637,7 +60000,7 @@ var FeatureProbeStatusSchema = object({
59637
60000
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59638
60001
  model: string().nullable(),
59639
60002
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59640
- channelCount: number().nullable(),
60003
+ channelCount: number().nullable().meta(readingMeta("count")),
59641
60004
  /**
59642
60005
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59643
60006
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59760,9 +60123,91 @@ var gasCapability = {
59760
60123
  * whether persisting is worth a SQLite commit. */
59761
60124
  volatileStateFields: ["lastChangedAt"]
59762
60125
  };
60126
+ var GroupCommandsSchema = object({
60127
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60128
+ results: array(object({
60129
+ deviceId: number().int().nonnegative(),
60130
+ cap: string().min(1),
60131
+ method: string().min(1),
60132
+ ok: boolean(),
60133
+ error: string().optional(),
60134
+ /** Ms epoch of the attempt. */
60135
+ at: number()
60136
+ })) });
60137
+ var groupCommandsCapability = {
60138
+ name: "group-commands",
60139
+ scope: "device",
60140
+ deviceNative: true,
60141
+ mode: "singleton",
60142
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60143
+ methods: {},
60144
+ status: {
60145
+ schema: GroupCommandsSchema,
60146
+ kind: "push"
60147
+ },
60148
+ runtimeState: GroupCommandsSchema,
60149
+ /**
60150
+ * Runtime-state durability: **session** — the last attempt. A restored
60151
+ * failure would outlive the command that caused it.
60152
+ *
60153
+ * See `RuntimeStateDurability`. Enforced by
60154
+ * `scripts/check-runtime-state-durability.ts`.
60155
+ */
60156
+ durability: "session",
60157
+ history: "none"
60158
+ };
60159
+ /**
60160
+ * How many members of a helper group answered (D944). Counts are integers,
60161
+ * not measurements: zero included is a real count, never a stand-in reading.
60162
+ * The reading itself stays on the output capability. History is none — the
60163
+ * series that later statistics read is the output cap's own history block.
60164
+ */
60165
+ var GroupStatusSchema = object({
60166
+ /** Members whose current value entered the aggregate. */
60167
+ included: number().int().nonnegative().meta(readingMeta("count", {
60168
+ precision: 0,
60169
+ label: "Reporting"
60170
+ })),
60171
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60172
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60173
+ precision: 0,
60174
+ label: "Excluded"
60175
+ })),
60176
+ /** `included` + `excluded`. */
60177
+ total: number().int().nonnegative().meta(readingMeta("count", {
60178
+ precision: 0,
60179
+ label: "Members"
60180
+ }))
60181
+ });
60182
+ var groupStatusCapability = {
60183
+ name: "group-status",
60184
+ scope: "device",
60185
+ deviceNative: true,
60186
+ mode: "singleton",
60187
+ deviceTypes: [
60188
+ DeviceType.Sensor,
60189
+ DeviceType.Switch,
60190
+ DeviceType.Light
60191
+ ],
60192
+ methods: {},
60193
+ status: {
60194
+ schema: GroupStatusSchema,
60195
+ kind: "push"
60196
+ },
60197
+ runtimeState: GroupStatusSchema,
60198
+ /**
60199
+ * Runtime-state durability: **session** — recomputed from members on the
60200
+ * next source event; a restored count would outlive the readings it named.
60201
+ *
60202
+ * See `RuntimeStateDurability`. Enforced by
60203
+ * `scripts/check-runtime-state-durability.ts`.
60204
+ */
60205
+ durability: "session",
60206
+ history: "none"
60207
+ };
59763
60208
  var HeatingValveStatusSchema = object({
59764
60209
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59765
- position: number().min(0).max(100).nullable(),
60210
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59766
60211
  /** Ms epoch when the slice was last updated (push or poll). */
59767
60212
  lastFetchedAt: number()
59768
60213
  });
@@ -59805,9 +60250,9 @@ var HumidifierStatusSchema = object({
59805
60250
  /** Whether the humidifier is currently on. */
59806
60251
  on: boolean(),
59807
60252
  /** Current measured relative humidity (0..100). Null when not reported. */
59808
- currentHumidity: number().min(0).max(100).nullable(),
60253
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59809
60254
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59810
- targetHumidity: number().min(0).max(100).nullable(),
60255
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59811
60256
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59812
60257
  * device has no mode surface. */
59813
60258
  mode: string().nullable(),
@@ -59817,9 +60262,9 @@ var HumidifierStatusSchema = object({
59817
60262
  * `idle` / `off`). Null when the device doesn't report it. */
59818
60263
  action: string().nullable(),
59819
60264
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59820
- minHumidity: number().nullable(),
60265
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59821
60266
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59822
- maxHumidity: number().nullable(),
60267
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59823
60268
  /** Ms epoch when the slice was last updated. */
59824
60269
  lastChangedAt: number()
59825
60270
  });
@@ -59889,22 +60334,13 @@ var humidifierCapability = {
59889
60334
  * `sourceInfo`.
59890
60335
  */
59891
60336
  var HumiditySensorStatusSchema = object({
59892
- /** Current relative humidity, 0..100. */
59893
- percent: number().min(0).max(100).meta({
59894
- unit: "%",
60337
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60338
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60339
+ precision: 0,
59895
60340
  label: "Humidity"
59896
- }),
60341
+ })),
59897
60342
  /** Ms epoch when the slice was last updated. */
59898
- lastFetchedAt: number(),
59899
- /** Live display unit from the upstream source (e.g. HA
59900
- * `attributes.unit_of_measurement`). The UI prefers this over the
59901
- * role's canonical unit. Absent → fall back to the canonical unit. */
59902
- unit: string().optional(),
59903
- /** Suggested decimal places for numeric display.
59904
- * Populated live from the upstream source when provided (e.g. HA
59905
- * `attributes.suggested_display_precision`). Falls back to
59906
- * auto-formatting when absent. */
59907
- precision: number().int().min(0).max(10).optional()
60343
+ lastFetchedAt: number()
59908
60344
  });
59909
60345
  var humiditySensorCapability = {
59910
60346
  name: "humidity-sensor",
@@ -59935,12 +60371,7 @@ var humiditySensorCapability = {
59935
60371
  } } },
59936
60372
  /** Clock fields: written, but excluded from the compare that decides
59937
60373
  * whether persisting is worth a SQLite commit. */
59938
- volatileStateFields: ["lastFetchedAt"],
59939
- customize: {
59940
- version: 1,
59941
- advanced: "derive",
59942
- targets: []
59943
- }
60374
+ volatileStateFields: ["lastFetchedAt"]
59944
60375
  };
59945
60376
  /**
59946
60377
  * Image display cap. Models a single still image exposed by an integration —
@@ -59961,7 +60392,7 @@ var ImageStatusSchema = object({
59961
60392
  * entity exposes no `entity_picture` (yet). */
59962
60393
  url: string().nullable(),
59963
60394
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59964
- lastUpdated: number().nullable()
60395
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59965
60396
  });
59966
60397
  var imageCapability = {
59967
60398
  name: "image",
@@ -60049,19 +60480,19 @@ var NormalizedRangeSchema = object({
60049
60480
  */
60050
60481
  var ImageSettingsStatusSchema = object({
60051
60482
  /** Normalized 0–100. */
60052
- brightness: number().optional(),
60483
+ brightness: number().optional().meta(readingMeta("none")),
60053
60484
  /** Normalized 0–100. */
60054
- contrast: number().optional(),
60485
+ contrast: number().optional().meta(readingMeta("none")),
60055
60486
  /** Normalized 0–100. */
60056
- saturation: number().optional(),
60487
+ saturation: number().optional().meta(readingMeta("none")),
60057
60488
  /** Normalized 0–100. */
60058
- sharpness: number().optional(),
60489
+ sharpness: number().optional().meta(readingMeta("none")),
60059
60490
  mirror: boolean().optional(),
60060
60491
  flip: boolean().optional(),
60061
60492
  rotate: ImageRotateSchema.optional(),
60062
60493
  whiteBalance: WhiteBalanceModeSchema.optional(),
60063
60494
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60064
- warmth: number().optional(),
60495
+ warmth: number().optional().meta(readingMeta("none")),
60065
60496
  exposureMode: ExposureModeSchema.optional(),
60066
60497
  backlightMode: BacklightModeSchema.optional(),
60067
60498
  lastFetchedAt: number()
@@ -60340,10 +60771,10 @@ var IntercomAbilitySchema = object({
60340
60771
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60341
60772
  codecs: array(string()),
60342
60773
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60343
- sampleRate: number(),
60774
+ sampleRate: number().meta(readingMeta("none")),
60344
60775
  duplex: _enum(["half", "full"]),
60345
60776
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60346
- maxBacklogMs: number()
60777
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60347
60778
  });
60348
60779
  var IntercomStatusSchema = object({
60349
60780
  /** True while a talk session is open. */
@@ -60570,13 +61001,13 @@ var LawnMowerControlStatusSchema = object({
60570
61001
  activity: LawnMowerActivitySchema,
60571
61002
  /** 0..100 battery percentage. Null when the device has no battery
60572
61003
  * reading. */
60573
- batteryLevel: number().min(0).max(100).nullable(),
61004
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60574
61005
  /** 0..100 mowing-completion percentage of the current task, or null when no
60575
61006
  * task is active / progress is unavailable. */
60576
- progressPercent: number().min(0).max(100).nullable(),
61007
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60577
61008
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60578
61009
  * or null when unknown. */
60579
- currentCode: number().nullable(),
61010
+ currentCode: number().nullable().meta(readingMeta("none")),
60580
61011
  /** Human label for {@link currentCode}, or null when undecodable. */
60581
61012
  currentCodeLabel: string().nullable(),
60582
61013
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61109,7 +61540,7 @@ var MediaPlayerStatusSchema = object({
61109
61540
  state: MediaPlayerStateSchema,
61110
61541
  /** Volume as 0..100 inclusive. Null when the device has no volume
61111
61542
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61112
- volumeLevel: number().min(0).max(100).nullable(),
61543
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61113
61544
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61114
61545
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61115
61546
  isMuted: boolean().nullable(),
@@ -61122,10 +61553,10 @@ var MediaPlayerStatusSchema = object({
61122
61553
  currentMedia: MediaInfoSchema.nullable(),
61123
61554
  /** Current playback position in ms. Null when not seekable or
61124
61555
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61125
- positionMs: number().int().nonnegative().nullable(),
61556
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61126
61557
  /** Total duration of the current media in ms. Null when unknown
61127
61558
  * (live stream) or nothing is playing. */
61128
- durationMs: number().int().nonnegative().nullable(),
61559
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61129
61560
  /** Shuffle toggle. Null when no shuffle surface.
61130
61561
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61131
61562
  shuffle: boolean().nullable(),
@@ -61434,7 +61865,7 @@ var motionCustomize = {
61434
61865
  eventName: "onMotionChanged"
61435
61866
  },
61436
61867
  deviceTypes: [DeviceType.Camera],
61437
- apply: "replace",
61868
+ apply: "add",
61438
61869
  applyAuthority: {
61439
61870
  kind: "camera-sensors",
61440
61871
  timeoutField: "motionCooldownMs"
@@ -61506,7 +61937,7 @@ var MotionStatusSchema = object({
61506
61937
  * it, every provider seeded it at boot, and every session left it
61507
61938
  * `detected: true` after its detach.
61508
61939
  */
61509
- autoClearAfterMs: number().nullable()
61940
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61510
61941
  });
61511
61942
  /**
61512
61943
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61543,12 +61974,18 @@ var motionCapability = {
61543
61974
  methods: {
61544
61975
  /**
61545
61976
  * Pull the current motion state synchronously. Convenience shortcut
61546
- * for consumers that don't need the full status object; equivalent
61547
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61548
- * `getStatus` once the auto-injected status surface lands in every
61549
- * consumer.
61977
+ * for consumers that don't need the full status object. A composed
61978
+ * group answers from its slice (D944): unknown throws, it is not off.
61550
61979
  */
61551
61980
  isDetected: method(object({ deviceId: number() }), boolean()) },
61981
+ /**
61982
+ * A composed motion group serves `isDetected` from `detected` (D944).
61983
+ * Without this the cap stays uncomposable (`has-commands`).
61984
+ */
61985
+ composedMethods: { isDetected: {
61986
+ kind: "slice",
61987
+ field: "detected"
61988
+ } },
61552
61989
  events: {
61553
61990
  /**
61554
61991
  * Fires every time the runner transitions a camera between
@@ -61688,7 +62125,7 @@ var MotionZoneRegionSchema = object({
61688
62125
  * the grid region(s). */
61689
62126
  var MotionZoneStatusSchema = object({
61690
62127
  enabled: boolean(),
61691
- sensitivity: number(),
62128
+ sensitivity: number().meta(readingMeta("none")),
61692
62129
  /** Grid region(s). Today exactly one `grid` shape. */
61693
62130
  regions: array(MotionZoneRegionSchema),
61694
62131
  lastFetchedAt: number()
@@ -62058,7 +62495,7 @@ var NavigationStatusSchema = object({
62058
62495
  following: boolean(),
62059
62496
  flash: boolean(),
62060
62497
  lightMode: NavigationLightModeSchema,
62061
- lightLevel: number().min(40).max(100),
62498
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62062
62499
  /** Ms epoch when the slice was last updated. */
62063
62500
  lastChangedAt: number()
62064
62501
  });
@@ -62192,13 +62629,13 @@ var NetworkLinkStatusSchema = object({
62192
62629
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62193
62630
  * SKIP a null rather than coerce it.
62194
62631
  */
62195
- signalPercent: number().min(0).max(100).nullable(),
62632
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62196
62633
  /** Raw received signal strength in dBm, when the firmware reports one. */
62197
- rssiDbm: number().optional(),
62634
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62198
62635
  /** Network name of a wireless link, when the firmware reports it. */
62199
62636
  ssid: string().optional(),
62200
62637
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62201
- lastUpdated: number()
62638
+ lastUpdated: number().meta(readingMeta("none"))
62202
62639
  });
62203
62640
  var networkLinkCapability = {
62204
62641
  name: "network-link",
@@ -62583,7 +63020,7 @@ var NotifierStatusSchema = object({
62583
63020
  lastError: string().nullable(),
62584
63021
  /** Number of deliveries currently buffered server-side (rate-limited
62585
63022
  * notifiers may queue). 0 when the provider sends synchronously. */
62586
- queueDepth: number().int().nonnegative(),
63023
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62587
63024
  /** Per-feature capability matrix — authoritative source for the
62588
63025
  * compose-form field gating. */
62589
63026
  supports: NotifierSupportsSchema
@@ -62675,11 +63112,9 @@ onSent: { data: object({
62675
63112
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62676
63113
  * for: water flow, distance, weight, frequency, signal strength, …).
62677
63114
  *
62678
- * The `unit` and `precision` fields carry the live display metadata that
62679
- * the upstream source provides with each state push (HA
62680
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62681
- * The UI reads them directly from the slice — they are the single source
62682
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63115
+ * The quantity is not on this slice. It is chosen when the device is
63116
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63117
+ * that quantity's base unit. `null` is a refused sample (D393).
62683
63118
  *
62684
63119
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62685
63120
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62688,15 +63123,8 @@ onSent: { data: object({
62688
63123
  * numeric values land here.
62689
63124
  */
62690
63125
  var NumericSensorStatusSchema = object({
62691
- value: number(),
62692
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62693
- * Populated live from the upstream source on each state push.
62694
- * Absent for unitless sensors. */
62695
- unit: string().optional(),
62696
- /** Suggested decimal places for numeric display.
62697
- * Populated live from the upstream source when provided.
62698
- * Falls back to auto-formatting when absent. */
62699
- precision: number().int().min(0).max(10).optional(),
63126
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63127
+ value: number().nullable().meta(readingMeta("device")),
62700
63128
  /** Ms epoch when the slice was last updated. */
62701
63129
  lastFetchedAt: number()
62702
63130
  });
@@ -62720,11 +63148,11 @@ var numericSensorCapability = {
62720
63148
  */
62721
63149
  durability: "restored",
62722
63150
  /**
62723
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63151
+ * History (D850): recorded series. The unit is the device quantity's base,
63152
+ * resolved when the series is read — the cap has no single unit.
62724
63153
  */
62725
63154
  history: { fields: { value: {
62726
63155
  kind: "gauge",
62727
- unitFrom: "unit",
62728
63156
  minIntervalMs: 3e4
62729
63157
  } } },
62730
63158
  /** Clock fields: written, but excluded from the compare that decides
@@ -63016,23 +63444,26 @@ var PetFeederStatusSchema = object({
63016
63444
  /** Food currently in the bowl (grams). Null when the device has not
63017
63445
  * reported a reading yet. On dual-hopper models this is the combined
63018
63446
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63019
- foodLevel: number().nullable(),
63447
+ foodLevel: number().nullable().meta(readingMeta("none")),
63020
63448
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63021
63449
  * single-hopper models. */
63022
- food1: number().nullable(),
63450
+ food1: number().nullable().meta(readingMeta("none")),
63023
63451
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63024
63452
  * single-hopper models. */
63025
- food2: number().nullable(),
63453
+ food2: number().nullable().meta(readingMeta("none")),
63026
63454
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63027
63455
  * (`device_class: problem`, on = low). True when the bowl is empty /
63028
63456
  * below the feeder's low threshold. */
63029
63457
  lowFood: boolean(),
63030
63458
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63031
63459
  * device has no battery reading. */
63032
- batteryPower: number().min(0).max(100).nullable(),
63033
- /** Days of desiccant life remaining. Null when the model has no
63034
- * desiccant sensor. */
63035
- desiccantLeftDays: number().nullable(),
63460
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63461
+ /**
63462
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63463
+ * Providers convert from the days the feeder reports. Null when the
63464
+ * model has no desiccant sensor.
63465
+ */
63466
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63036
63467
  /** True while a feed is in progress. */
63037
63468
  feeding: boolean(),
63038
63469
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63043,7 +63474,7 @@ var PetFeederStatusSchema = object({
63043
63474
  * with `errorCode` for consumers that want the raw integer. */
63044
63475
  error: string().nullable(),
63045
63476
  /** Raw device error code (0 / null = no error). */
63046
- errorCode: number().nullable(),
63477
+ errorCode: number().nullable().meta(readingMeta("none")),
63047
63478
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63048
63479
  isDualHopper: boolean(),
63049
63480
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63053,7 +63484,7 @@ var PetFeederStatusSchema = object({
63053
63484
  /** Play a chime when dispensing. */
63054
63485
  feedSound: boolean(),
63055
63486
  /** Speaker / prompt volume level (device-scaled integer). */
63056
- volume: number(),
63487
+ volume: number().meta(readingMeta("none")),
63057
63488
  /** Ms epoch when the slice was last refreshed from the cloud. */
63058
63489
  lastFetchedAt: number()
63059
63490
  });
@@ -63172,7 +63603,7 @@ var petFeederCapability = {
63172
63603
  },
63173
63604
  desiccantLeftDays: {
63174
63605
  kind: "gauge",
63175
- unit: "days"
63606
+ unit: "s"
63176
63607
  }
63177
63608
  } }
63178
63609
  };
@@ -63573,36 +64004,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63573
64004
  */
63574
64005
  var PowerMeterStatusSchema = object({
63575
64006
  /** Instantaneous power draw in watts. */
63576
- watts: number().optional().meta({
63577
- unit: "W",
63578
- label: "Power"
63579
- }),
64007
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63580
64008
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63581
- kwhTotal: number().optional().meta({
63582
- unit: "kWh",
63583
- label: "Energy"
63584
- }),
64009
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63585
64010
  /** Voltage in volts. */
63586
- volts: number().optional(),
64011
+ volts: number().optional().meta(readingMeta("voltage")),
63587
64012
  /** Current in amperes. */
63588
- amps: number().optional(),
63589
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63590
- va: number().optional(),
64013
+ amps: number().optional().meta(readingMeta("current")),
64014
+ /** Apparent power in volt-amperes. */
64015
+ va: number().optional().meta(readingMeta("apparent-power")),
63591
64016
  /** Power factor (0..1) if reported. */
63592
- powerFactor: number().min(0).max(1).optional(),
64017
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63593
64018
  /** Ms epoch when the slice was last updated. */
63594
- lastFetchedAt: number(),
63595
- /** Live display unit of the single metric this slice carries (e.g. HA
63596
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63597
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63598
- * unit per slice is unambiguous. The UI prefers this over the role's
63599
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63600
- unit: string().optional(),
63601
- /** Suggested decimal places for numeric display.
63602
- * Populated live from the upstream source when provided (e.g. HA
63603
- * `attributes.suggested_display_precision`). Falls back to
63604
- * auto-formatting when absent. */
63605
- precision: number().int().min(0).max(10).optional()
64019
+ lastFetchedAt: number()
63606
64020
  });
63607
64021
  var powerMeterCapability = {
63608
64022
  name: "power-meter",
@@ -63666,12 +64080,7 @@ var powerMeterCapability = {
63666
64080
  } },
63667
64081
  /** Clock fields: written, but excluded from the compare that decides
63668
64082
  * whether persisting is worth a SQLite commit. */
63669
- volatileStateFields: ["lastFetchedAt"],
63670
- customize: {
63671
- version: 1,
63672
- advanced: "derive",
63673
- targets: []
63674
- }
64083
+ volatileStateFields: ["lastFetchedAt"]
63675
64084
  };
63676
64085
  /**
63677
64086
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63691,11 +64100,11 @@ var powerMeterCapability = {
63691
64100
  */
63692
64101
  var GpsLocationSchema = object({
63693
64102
  /** Latitude in decimal degrees, -90..90. */
63694
- latitude: number().min(-90).max(90),
64103
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63695
64104
  /** Longitude in decimal degrees, -180..180. */
63696
- longitude: number().min(-180).max(180),
64105
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63697
64106
  /** Reported accuracy in meters (lower = better). */
63698
- accuracyMeters: number().nonnegative()
64107
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63699
64108
  });
63700
64109
  var PresenceStatusSchema = object({
63701
64110
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63707,7 +64116,7 @@ var PresenceStatusSchema = object({
63707
64116
  gps: GpsLocationSchema.nullable(),
63708
64117
  /** Optional battery level of the tracking device (0..100). Null
63709
64118
  * when not reported. */
63710
- batteryPercent: number().min(0).max(100).nullable(),
64119
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63711
64120
  /** Ms epoch when the slice was last updated. */
63712
64121
  lastChangedAt: number()
63713
64122
  });
@@ -63757,19 +64166,10 @@ var presenceCapability = {
63757
64166
  * `sourceInfo`.
63758
64167
  */
63759
64168
  var PressureSensorStatusSchema = object({
63760
- /** Current pressure in hPa. */
63761
- hpa: number(),
64169
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64170
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63762
64171
  /** Ms epoch when the slice was last updated. */
63763
- lastFetchedAt: number(),
63764
- /** Live display unit from the upstream source (e.g. HA
63765
- * `attributes.unit_of_measurement`). The UI prefers this over the
63766
- * role's canonical unit. Absent → fall back to the canonical unit. */
63767
- unit: string().optional(),
63768
- /** Suggested decimal places for numeric display.
63769
- * Populated live from the upstream source when provided (e.g. HA
63770
- * `attributes.suggested_display_precision`). Falls back to
63771
- * auto-formatting when absent. */
63772
- precision: number().int().min(0).max(10).optional()
64172
+ lastFetchedAt: number()
63773
64173
  });
63774
64174
  var pressureSensorCapability = {
63775
64175
  name: "pressure-sensor",
@@ -64183,8 +64583,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64183
64583
  }), _void(), { kind: "mutation" });
64184
64584
  var PtzAutotrackSettingsSchema = object({
64185
64585
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64186
- stopDelaySeconds: number().int().min(0).max(300),
64187
- disappearDelaySeconds: number().int().min(0).max(300)
64586
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64587
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64188
64588
  });
64189
64589
  /**
64190
64590
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64338,7 +64738,7 @@ var RunningStateStatusSchema = object({
64338
64738
  /** null = unknown — never coerced to false (D393). */
64339
64739
  running: boolean().nullable(),
64340
64740
  /** Epoch ms the current boolean began; null while `running` is null. */
64341
- since: number().nullable(),
64741
+ since: number().nullable().meta(readingMeta("none")),
64342
64742
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64343
64743
  phase: string().nullable(),
64344
64744
  /** null until a reading has been published (D393). */
@@ -64353,7 +64753,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64353
64753
  /** Generic Customize target for the running boolean (D936). */
64354
64754
  var runningCustomize = {
64355
64755
  version: 1,
64356
- advanced: "derive",
64357
64756
  targets: [{
64358
64757
  id: "running",
64359
64758
  cardinality: "scalar",
@@ -64373,7 +64772,7 @@ var runningCustomize = {
64373
64772
  DeviceType.Thermostat,
64374
64773
  DeviceType.PetFeeder
64375
64774
  ],
64376
- apply: "add-or-replace",
64775
+ apply: "add",
64377
64776
  path: "running",
64378
64777
  sources: [
64379
64778
  {
@@ -65637,9 +66036,9 @@ var RecordTriggerSchema = _enum([
65637
66036
  */
65638
66037
  var RecordWindowSchema = object({
65639
66038
  trigger: RecordTriggerSchema,
65640
- day: number().int().min(0).max(6),
65641
- startMinute: number().int().min(0).max(1439),
65642
- endMinute: number().int().min(1).max(1440)
66039
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66040
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66041
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65643
66042
  });
65644
66043
  /** Status of one physical volume, as the camera itself describes it. */
65645
66044
  var OnboardStorageVolumeSchema = object({
@@ -65715,10 +66114,10 @@ var RecordingOnboardStatusSchema = object({
65715
66114
  /** Master "record to the card at all" switch. */
65716
66115
  enabled: boolean().nullable(),
65717
66116
  overwriteWhenFull: boolean().nullable(),
65718
- preRecordSec: number().nullable(),
65719
- postRecordSec: number().nullable(),
66117
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66118
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65720
66119
  /** Length of one recorded file, in minutes. */
65721
- segmentMinutes: number().nullable(),
66120
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65722
66121
  /** The primary track's weekly windows, flattened. */
65723
66122
  windows: array(RecordWindowSchema),
65724
66123
  /**
@@ -65734,7 +66133,7 @@ var RecordingOnboardStatusSchema = object({
65734
66133
  * provider reporting a non-zero count also reports the schedule as not
65735
66134
  * writable.
65736
66135
  */
65737
- unreadableWindows: number(),
66136
+ unreadableWindows: number().meta(readingMeta("count")),
65738
66137
  /**
65739
66138
  * The camera is scheduled to record and has NO usable storage.
65740
66139
  *
@@ -66579,7 +66978,7 @@ var CamStreamDescriptorSchema = object({
66579
66978
  url: string().optional(),
66580
66979
  codec: string().optional(),
66581
66980
  resolution: CamStreamResolutionSchema.optional(),
66582
- fps: number().positive().optional(),
66981
+ fps: number().positive().optional().meta(readingMeta("rate")),
66583
66982
  label: string().optional(),
66584
66983
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66585
66984
  deviceFeatures: array(string()).optional(),
@@ -66666,18 +67065,18 @@ var StreamProfileSchema = _enum([
66666
67065
  ]);
66667
67066
  /** The current encoding config of one stream profile. */
66668
67067
  var StreamProfileConfigSchema = object({
66669
- width: number(),
66670
- height: number(),
67068
+ width: number().meta(readingMeta("none")),
67069
+ height: number().meta(readingMeta("none")),
66671
67070
  codec: _enum(["h264", "h265"]),
66672
- framerate: number(),
66673
- bitrate: number(),
67071
+ framerate: number().meta(readingMeta("rate")),
67072
+ bitrate: number().meta(readingMeta("bitrate")),
66674
67073
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66675
67074
  encoderProfile: _enum([
66676
67075
  "high",
66677
67076
  "main",
66678
67077
  "baseline"
66679
67078
  ]).optional(),
66680
- gop: number().optional(),
67079
+ gop: number().optional().meta(readingMeta("none")),
66681
67080
  /**
66682
67081
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66683
67082
  *
@@ -66898,12 +67297,7 @@ var switchCapability = {
66898
67297
  label: "On",
66899
67298
  kind: "boolean"
66900
67299
  }
66901
- }] },
66902
- customize: {
66903
- version: 1,
66904
- advanced: "derive",
66905
- targets: []
66906
- }
67300
+ }] }
66907
67301
  };
66908
67302
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66909
67303
  function hasControlCharacter(value) {
@@ -67486,35 +67880,21 @@ var tamperCapability = {
67486
67880
  * entries with `device_class: temperature` and any future native
67487
67881
  * thermometer.
67488
67882
  *
67489
- * Unit normalisation: providers convert to Celsius before storing.
67490
- * The slice value is always Celsius so cross-cap aggregators
67491
- * (climate-control's `currentTemp`, energy analytics) can compose
67492
- * without per-source unit fixups. The canonical display unit (`°C`) is
67493
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67494
- * `sourceInfo`.
67883
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67884
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67495
67885
  *
67496
67886
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67497
67887
  * frozen feed (provider hung) distinct from a "temperature hasn't
67498
67888
  * changed" steady state.
67499
67889
  */
67500
67890
  var TemperatureSensorStatusSchema = object({
67501
- /** Current temperature in Celsius. */
67502
- celsius: number().meta({
67503
- unit: "°C",
67891
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67892
+ celsius: number().nullable().meta(readingMeta("temperature", {
67893
+ precision: 1,
67504
67894
  label: "Temperature"
67505
- }),
67895
+ })),
67506
67896
  /** Ms epoch when the slice was last updated (push or poll). */
67507
- lastFetchedAt: number(),
67508
- /** Live display unit from the upstream source (e.g. HA
67509
- * `attributes.unit_of_measurement`). The UI prefers this over the
67510
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67511
- * Absent → the UI falls back to the role's canonical unit. */
67512
- unit: string().optional(),
67513
- /** Suggested decimal places for numeric display.
67514
- * Populated live from the upstream source when provided (e.g. HA
67515
- * `attributes.suggested_display_precision`). Falls back to
67516
- * auto-formatting when absent. */
67517
- precision: number().int().min(0).max(10).optional()
67897
+ lastFetchedAt: number()
67518
67898
  });
67519
67899
  var temperatureSensorCapability = {
67520
67900
  name: "temperature-sensor",
@@ -67545,12 +67925,7 @@ var temperatureSensorCapability = {
67545
67925
  } } },
67546
67926
  /** Clock fields: written, but excluded from the compare that decides
67547
67927
  * whether persisting is worth a SQLite commit. */
67548
- volatileStateFields: ["lastFetchedAt"],
67549
- customize: {
67550
- version: 1,
67551
- advanced: "derive",
67552
- targets: []
67553
- }
67928
+ volatileStateFields: ["lastFetchedAt"]
67554
67929
  };
67555
67930
  /**
67556
67931
  * toast — system-scoped singleton capability that streams toast
@@ -67833,16 +68208,16 @@ var VacuumControlStatusSchema = object({
67833
68208
  ]),
67834
68209
  /** 0..100 battery percentage. Null when the device has no battery
67835
68210
  * reading. */
67836
- batteryLevel: number().min(0).max(100).nullable(),
68211
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67837
68212
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67838
68213
  * the vacuum has no speed control. */
67839
68214
  fanSpeed: string().nullable(),
67840
68215
  /** Speed tokens the hardware accepts — drives the UI selector. */
67841
68216
  availableFanSpeeds: array(string()),
67842
68217
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67843
- progressPercent: number().min(0).max(100).nullable(),
68218
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67844
68219
  /** Current error code (0 / null = no error). */
67845
- errorCode: number().nullable(),
68220
+ errorCode: number().nullable().meta(readingMeta("none")),
67846
68221
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67847
68222
  errorLabel: string().nullable(),
67848
68223
  /** Ms epoch when the slice was last updated. */
@@ -67920,7 +68295,7 @@ var ValveStatusSchema = object({
67920
68295
  ]),
67921
68296
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67922
68297
  * intermediate position surface. */
67923
- position: number().min(0).max(100).nullable(),
68298
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67924
68299
  /** Ms epoch when the slice was last updated. */
67925
68300
  lastChangedAt: number()
67926
68301
  });
@@ -68028,9 +68403,9 @@ var vibrationCapability = {
68028
68403
  */
68029
68404
  var WaterHeaterStatusSchema = object({
68030
68405
  /** Current measured temperature. Null when not reported. */
68031
- currentTemp: number().nullable(),
68406
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68032
68407
  /** Target temperature setpoint. Null when no setpoint surface. */
68033
- targetTemp: number().nullable(),
68408
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68034
68409
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68035
68410
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68036
68411
  * device reports an unknown state. */
@@ -68041,9 +68416,9 @@ var WaterHeaterStatusSchema = object({
68041
68416
  * has no away surface. */
68042
68417
  away: boolean().nullable(),
68043
68418
  /** HA `min_temp` attribute. Null when not reported. */
68044
- minTemp: number().nullable(),
68419
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68045
68420
  /** HA `max_temp` attribute. Null when not reported. */
68046
- maxTemp: number().nullable(),
68421
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68047
68422
  /** Ms epoch when the slice was last updated. */
68048
68423
  lastChangedAt: number()
68049
68424
  });
@@ -68128,25 +68503,18 @@ var waterHeaterCapability = {
68128
68503
  * Forecast deferred — current conditions only for v1.
68129
68504
  */
68130
68505
  var WeatherStatusSchema = object({
68131
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68132
- * when no condition has been reported yet. */
68506
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68133
68507
  condition: string().nullable(),
68134
- /** Current temperature in the reported unit. Null when not provided. */
68135
- temperature: number().nullable(),
68136
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68137
- temperatureUnit: string().nullable(),
68508
+ /** Current temperature in Celsius. Null when not provided or refused. */
68509
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68138
68510
  /** Relative humidity (0..100). Null when not provided. */
68139
- humidity: number().min(0).max(100).nullable(),
68140
- /** Barometric pressure in the reported unit. Null when not provided. */
68141
- pressure: number().nullable(),
68142
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68143
- pressureUnit: string().nullable(),
68144
- /** Wind speed in the reported unit. Null when not provided. */
68145
- windSpeed: number().nullable(),
68146
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68147
- windSpeedUnit: string().nullable(),
68511
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68512
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68513
+ pressure: number().nullable().meta(readingMeta("pressure")),
68514
+ /** Wind speed in metres per second. Null when not provided or refused. */
68515
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68148
68516
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68149
- windBearing: number().nullable(),
68517
+ windBearing: number().nullable().meta(readingMeta("angle")),
68150
68518
  /** Ms epoch when the slice was last updated. */
68151
68519
  lastFetchedAt: number()
68152
68520
  });
@@ -68174,12 +68542,12 @@ var weatherCapability = {
68174
68542
  */
68175
68543
  durability: "session",
68176
68544
  /**
68177
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68545
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68178
68546
  */
68179
68547
  history: { fields: {
68180
68548
  temperature: {
68181
68549
  kind: "gauge",
68182
- unitFrom: "temperatureUnit",
68550
+ unit: "°C",
68183
68551
  deadband: .2
68184
68552
  },
68185
68553
  humidity: {
@@ -68189,12 +68557,12 @@ var weatherCapability = {
68189
68557
  },
68190
68558
  pressure: {
68191
68559
  kind: "gauge",
68192
- unitFrom: "pressureUnit",
68560
+ unit: "hPa",
68193
68561
  deadband: .5
68194
68562
  },
68195
68563
  windSpeed: {
68196
68564
  kind: "gauge",
68197
- unitFrom: "windSpeedUnit",
68565
+ unit: "m/s",
68198
68566
  deadband: .5
68199
68567
  }
68200
68568
  } }
@@ -69425,6 +69793,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69425
69793
  featureProbe: featureProbeCapability,
69426
69794
  flood: floodCapability,
69427
69795
  gas: gasCapability,
69796
+ groupCommands: groupCommandsCapability,
69797
+ groupStatus: groupStatusCapability,
69428
69798
  heatingValve: heatingValveCapability,
69429
69799
  humidifier: humidifierCapability,
69430
69800
  humiditySensor: humiditySensorCapability,
@@ -70801,6 +71171,13 @@ Object.freeze({
70801
71171
  access: "view",
70802
71172
  role: "admin"
70803
71173
  },
71174
+ "addons.getPackageDocs": {
71175
+ capName: "addons",
71176
+ capScope: "system",
71177
+ addonId: null,
71178
+ access: "view",
71179
+ role: "admin"
71180
+ },
70804
71181
  "addons.getVersions": {
70805
71182
  capName: "addons",
70806
71183
  capScope: "system",
@@ -73909,6 +74286,20 @@ Object.freeze({
73909
74286
  access: "view",
73910
74287
  role: "viewer"
73911
74288
  },
74289
+ "groupCommands.getStatus": {
74290
+ capName: "group-commands",
74291
+ capScope: "device",
74292
+ addonId: null,
74293
+ access: "view",
74294
+ role: "viewer"
74295
+ },
74296
+ "groupStatus.getStatus": {
74297
+ capName: "group-status",
74298
+ capScope: "device",
74299
+ addonId: null,
74300
+ access: "view",
74301
+ role: "viewer"
74302
+ },
73912
74303
  "heatingValve.getStatus": {
73913
74304
  capName: "heating-valve",
73914
74305
  capScope: "device",
@@ -83731,6 +84122,37 @@ var SENSOR_CAP_VOCABULARY = {
83731
84122
  icon: "flame"
83732
84123
  }]
83733
84124
  },
84125
+ "group-commands": {
84126
+ scope: "device",
84127
+ noun: t("Group command", "Comando di gruppo")
84128
+ },
84129
+ "group-status": {
84130
+ scope: "device",
84131
+ noun: t("Group", "Gruppo"),
84132
+ state: [
84133
+ {
84134
+ kind: "numeric",
84135
+ field: "included",
84136
+ quantity: t("the members reporting", "i membri che rispondono"),
84137
+ label: t("Reporting", "Rispondono"),
84138
+ icon: "users"
84139
+ },
84140
+ {
84141
+ kind: "numeric",
84142
+ field: "excluded",
84143
+ quantity: t("the members left out", "i membri esclusi"),
84144
+ label: t("Excluded", "Esclusi"),
84145
+ icon: "users"
84146
+ },
84147
+ {
84148
+ kind: "numeric",
84149
+ field: "total",
84150
+ quantity: t("the member count", "il numero di membri"),
84151
+ label: t("Members", "Membri"),
84152
+ icon: "sigma"
84153
+ }
84154
+ ]
84155
+ },
83734
84156
  "humidity-sensor": {
83735
84157
  scope: "device",
83736
84158
  noun: t("Humidity", "Umidità"),
@@ -83764,7 +84186,6 @@ var SENSOR_CAP_VOCABULARY = {
83764
84186
  field: "value",
83765
84187
  quantity: t("the reading", "il valore"),
83766
84188
  label: t("Reading", "Valore"),
83767
- unitField: "unit",
83768
84189
  icon: "gauge"
83769
84190
  }]
83770
84191
  },
@@ -83900,7 +84321,7 @@ var SENSOR_CAP_VOCABULARY = {
83900
84321
  field: "temperature",
83901
84322
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83902
84323
  label: t("Temperature", "Temperatura"),
83903
- unitField: "temperatureUnit",
84324
+ unit: "°C",
83904
84325
  icon: "thermometer"
83905
84326
  },
83906
84327
  {
@@ -83916,7 +84337,7 @@ var SENSOR_CAP_VOCABULARY = {
83916
84337
  field: "windSpeed",
83917
84338
  quantity: t("the wind speed", "la velocità del vento"),
83918
84339
  label: t("Wind", "Vento"),
83919
- unitField: "windSpeedUnit",
84340
+ unit: "m/s",
83920
84341
  icon: "wind"
83921
84342
  },
83922
84343
  {
@@ -83924,7 +84345,7 @@ var SENSOR_CAP_VOCABULARY = {
83924
84345
  field: "pressure",
83925
84346
  quantity: t("the air pressure", "la pressione atmosferica"),
83926
84347
  label: t("Pressure", "Pressione"),
83927
- unitField: "pressureUnit",
84348
+ unit: "hPa",
83928
84349
  icon: "gauge"
83929
84350
  }
83930
84351
  ]
@@ -85197,6 +85618,16 @@ Object.freeze({
85197
85618
  form: "single",
85198
85619
  optional: false
85199
85620
  }],
85621
+ "groupCommands.getStatus": [{
85622
+ name: "deviceId",
85623
+ form: "single",
85624
+ optional: false
85625
+ }],
85626
+ "groupStatus.getStatus": [{
85627
+ name: "deviceId",
85628
+ form: "single",
85629
+ optional: false
85630
+ }],
85200
85631
  "heatingValve.getStatus": [{
85201
85632
  name: "deviceId",
85202
85633
  form: "single",
@@ -87181,6 +87612,8 @@ new Map(Object.entries({
87181
87612
  "detection-blind": "device-events",
87182
87613
  "motion-noisy": "device-events",
87183
87614
  "motion-calm": "device-events",
87615
+ "package-llm-silent": "device-events",
87616
+ "package-llm-answered": "device-events",
87184
87617
  "recording-stalled": "device-events",
87185
87618
  "recording-healthy": "device-events",
87186
87619
  "recording-degraded": "device-events",
@@ -87264,6 +87697,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87264
87697
  label: "Motion without a subject",
87265
87698
  kinds: ["motion-noisy", "motion-calm"]
87266
87699
  },
87700
+ {
87701
+ id: "package-llm",
87702
+ family: "device-events",
87703
+ label: "Package confirm",
87704
+ kinds: ["package-llm-silent", "package-llm-answered"]
87705
+ },
87267
87706
  {
87268
87707
  id: "recording",
87269
87708
  family: "device-events",
@@ -87445,6 +87884,8 @@ var DEVICE_EVENTS = [
87445
87884
  "detection-blind",
87446
87885
  "motion-noisy",
87447
87886
  "motion-calm",
87887
+ "package-llm-silent",
87888
+ "package-llm-answered",
87448
87889
  "recording-stalled",
87449
87890
  "recording-healthy",
87450
87891
  "recording-degraded",
@@ -87713,6 +88154,15 @@ new Map([
87713
88154
  snapshotLabel: "Cameras with constant motion now",
87714
88155
  snapshotSection: null
87715
88156
  },
88157
+ {
88158
+ id: "package-llm-confirm",
88159
+ family: "device-events",
88160
+ label: "Package confirm (not answering / answering again)",
88161
+ bad: ["package-llm-silent"],
88162
+ recovery: ["package-llm-answered"],
88163
+ snapshotLabel: "Cameras whose package confirm is not answering",
88164
+ snapshotSection: null
88165
+ },
87716
88166
  {
87717
88167
  id: "recording-health",
87718
88168
  family: "device-events",