@camstack/addon-provider-dreame 0.2.155 → 0.2.157

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -41,7 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
41
41
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
42
42
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
43
43
  //#endregion
44
- //#region ../types/dist/event-category-DHXxB8t9.mjs
44
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
45
45
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
46
46
  EventCategory["SystemBoot"] = "system.boot";
47
47
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -367,6 +367,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
367
367
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
368
368
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
369
369
  /**
370
+ * A camera's package LLM confirm has failed closed three times in a row
371
+ * (D931, 2026-10-26). One edge per camera until
372
+ * {@link PipelinePackageLlmAnswered}.
373
+ */
374
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
375
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
376
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
377
+ /**
370
378
  * Per-camera pipeline config was mutated by the orchestrator
371
379
  * (a settings change via `setAgentInferenceDevices` /
372
380
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5476,7 +5484,7 @@ var ZodIssueCode$1 = {
5476
5484
  var ZodFirstPartyTypeKind$1;
5477
5485
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5478
5486
  //#endregion
5479
- //#region ../types/dist/sleep-BErAxhul.mjs
5487
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5480
5488
  /**
5481
5489
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5482
5490
  * window to float samples (D455).
@@ -8030,6 +8038,99 @@ var RecordKindDeclarationSchema = object({
8030
8038
  }).strict();
8031
8039
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8032
8040
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8041
+ var READING_QUANTITIES = [
8042
+ "temperature",
8043
+ "pressure",
8044
+ "speed",
8045
+ "precipitation",
8046
+ "precipitation-rate",
8047
+ "length",
8048
+ "area",
8049
+ "illuminance",
8050
+ "irradiance",
8051
+ "power",
8052
+ "energy",
8053
+ "voltage",
8054
+ "current",
8055
+ "apparent-power",
8056
+ "humidity",
8057
+ "percentage",
8058
+ "concentration-volume",
8059
+ "concentration-mass",
8060
+ "concentration-ppb",
8061
+ "volume",
8062
+ "duration",
8063
+ "duration-ms",
8064
+ "information",
8065
+ "sound-level",
8066
+ "signal-level",
8067
+ "colour-temperature",
8068
+ "angle",
8069
+ "bitrate",
8070
+ "rate",
8071
+ "mass",
8072
+ "index",
8073
+ "ratio",
8074
+ "count",
8075
+ "none",
8076
+ "device"
8077
+ ];
8078
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8079
+ new Set(READING_QUANTITIES);
8080
+ /**
8081
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8082
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8083
+ */
8084
+ var QUANTITY_BASE = {
8085
+ temperature: "°C",
8086
+ pressure: "hPa",
8087
+ speed: "m/s",
8088
+ precipitation: "mm",
8089
+ "precipitation-rate": "mm/h",
8090
+ length: "m",
8091
+ area: "m²",
8092
+ illuminance: "lx",
8093
+ irradiance: "W/m²",
8094
+ power: "W",
8095
+ energy: "kWh",
8096
+ voltage: "V",
8097
+ current: "A",
8098
+ "apparent-power": "VA",
8099
+ humidity: "%",
8100
+ percentage: "%",
8101
+ "concentration-volume": "ppm",
8102
+ "concentration-mass": "µg/m³",
8103
+ "concentration-ppb": "ppb",
8104
+ volume: "L",
8105
+ duration: "s",
8106
+ "duration-ms": "ms",
8107
+ information: "B",
8108
+ "sound-level": "dBFS",
8109
+ "signal-level": "dBm",
8110
+ "colour-temperature": "mired",
8111
+ angle: "°",
8112
+ bitrate: "kbps",
8113
+ rate: "fps",
8114
+ mass: "kg",
8115
+ index: "none",
8116
+ ratio: "none",
8117
+ count: "none",
8118
+ none: "none",
8119
+ device: "device"
8120
+ };
8121
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8122
+ function baseUnitOf(quantity) {
8123
+ const symbol = QUANTITY_BASE[quantity];
8124
+ if (symbol === "none" || symbol === "device") return void 0;
8125
+ return symbol;
8126
+ }
8127
+ function readingMeta(quantity, extras) {
8128
+ return {
8129
+ quantity,
8130
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8131
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8132
+ };
8133
+ }
8033
8134
  /** Shared Zod schemas used across streaming capabilities. */
8034
8135
  var CamProfileSchema = _enum([
8035
8136
  "high",
@@ -8057,8 +8158,8 @@ var CamStreamKindSchema = _enum([
8057
8158
  "derived"
8058
8159
  ]);
8059
8160
  var CamStreamResolutionSchema = object({
8060
- width: number().int().positive(),
8061
- height: number().int().positive()
8161
+ width: number().int().positive().meta(readingMeta("none")),
8162
+ height: number().int().positive().meta(readingMeta("none"))
8062
8163
  });
8063
8164
  var CameraStreamSchema = object({
8064
8165
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8576,6 +8677,8 @@ var CAP_NAMES = Object.freeze([
8576
8677
  "filesystem-browse",
8577
8678
  "flood",
8578
8679
  "gas",
8680
+ "group-commands",
8681
+ "group-status",
8579
8682
  "heating-valve",
8580
8683
  "humidifier",
8581
8684
  "humidity-sensor",
@@ -8705,14 +8808,6 @@ Object.freeze({
8705
8808
  {
8706
8809
  "path": "aqi",
8707
8810
  "valueType": "number"
8708
- },
8709
- {
8710
- "path": "unit",
8711
- "valueType": "string"
8712
- },
8713
- {
8714
- "path": "precision",
8715
- "valueType": "number"
8716
8811
  }
8717
8812
  ],
8718
8813
  "alarm-panel": [
@@ -8748,20 +8843,10 @@ Object.freeze({
8748
8843
  "valueType": "boolean"
8749
8844
  }
8750
8845
  ],
8751
- "ambient-light-sensor": [
8752
- {
8753
- "path": "lux",
8754
- "valueType": "number"
8755
- },
8756
- {
8757
- "path": "unit",
8758
- "valueType": "string"
8759
- },
8760
- {
8761
- "path": "precision",
8762
- "valueType": "number"
8763
- }
8764
- ],
8846
+ "ambient-light-sensor": [{
8847
+ "path": "lux",
8848
+ "valueType": "number"
8849
+ }],
8765
8850
  "audio-metrics": [
8766
8851
  {
8767
8852
  "path": "ts",
@@ -8841,6 +8926,10 @@ Object.freeze({
8841
8926
  "path": "lastUpdated",
8842
8927
  "valueType": "number"
8843
8928
  },
8929
+ {
8930
+ "path": "volts",
8931
+ "valueType": "number"
8932
+ },
8844
8933
  {
8845
8934
  "path": "lastContactAt",
8846
8935
  "valueType": "number"
@@ -9083,10 +9172,6 @@ Object.freeze({
9083
9172
  "datetime"
9084
9173
  ]
9085
9174
  },
9086
- {
9087
- "path": "unit",
9088
- "valueType": "string"
9089
- },
9090
9175
  {
9091
9176
  "path": "min",
9092
9177
  "valueType": "number"
@@ -9098,19 +9183,6 @@ Object.freeze({
9098
9183
  {
9099
9184
  "path": "step",
9100
9185
  "valueType": "number"
9101
- },
9102
- {
9103
- "path": "precision",
9104
- "valueType": "number"
9105
- },
9106
- {
9107
- "path": "format",
9108
- "valueType": "enum",
9109
- "enumValues": [
9110
- "date",
9111
- "time",
9112
- "datetime"
9113
- ]
9114
9186
  }
9115
9187
  ],
9116
9188
  "cover": [
@@ -9351,13 +9423,8 @@ Object.freeze({
9351
9423
  "path": "value",
9352
9424
  "valueType": "string"
9353
9425
  }, {
9354
- "path": "format",
9355
- "valueType": "enum",
9356
- "enumValues": [
9357
- "date",
9358
- "time",
9359
- "datetime"
9360
- ]
9426
+ "path": "timestamp",
9427
+ "valueType": "boolean"
9361
9428
  }],
9362
9429
  "event-emitter": [
9363
9430
  {
@@ -9430,6 +9497,20 @@ Object.freeze({
9430
9497
  "path": "detected",
9431
9498
  "valueType": "boolean"
9432
9499
  }],
9500
+ "group-status": [
9501
+ {
9502
+ "path": "included",
9503
+ "valueType": "number"
9504
+ },
9505
+ {
9506
+ "path": "excluded",
9507
+ "valueType": "number"
9508
+ },
9509
+ {
9510
+ "path": "total",
9511
+ "valueType": "number"
9512
+ }
9513
+ ],
9433
9514
  "heating-valve": [{
9434
9515
  "path": "position",
9435
9516
  "valueType": "number"
@@ -9464,20 +9545,10 @@ Object.freeze({
9464
9545
  "valueType": "number"
9465
9546
  }
9466
9547
  ],
9467
- "humidity-sensor": [
9468
- {
9469
- "path": "percent",
9470
- "valueType": "number"
9471
- },
9472
- {
9473
- "path": "unit",
9474
- "valueType": "string"
9475
- },
9476
- {
9477
- "path": "precision",
9478
- "valueType": "number"
9479
- }
9480
- ],
9548
+ "humidity-sensor": [{
9549
+ "path": "percent",
9550
+ "valueType": "number"
9551
+ }],
9481
9552
  "image": [{
9482
9553
  "path": "url",
9483
9554
  "valueType": "string"
@@ -9816,20 +9887,10 @@ Object.freeze({
9816
9887
  "valueType": "boolean"
9817
9888
  }
9818
9889
  ],
9819
- "numeric-sensor": [
9820
- {
9821
- "path": "value",
9822
- "valueType": "number"
9823
- },
9824
- {
9825
- "path": "unit",
9826
- "valueType": "string"
9827
- },
9828
- {
9829
- "path": "precision",
9830
- "valueType": "number"
9831
- }
9832
- ],
9890
+ "numeric-sensor": [{
9891
+ "path": "value",
9892
+ "valueType": "number"
9893
+ }],
9833
9894
  "pet-feeder": [
9834
9895
  {
9835
9896
  "path": "foodLevel",
@@ -9921,14 +9982,6 @@ Object.freeze({
9921
9982
  {
9922
9983
  "path": "powerFactor",
9923
9984
  "valueType": "number"
9924
- },
9925
- {
9926
- "path": "unit",
9927
- "valueType": "string"
9928
- },
9929
- {
9930
- "path": "precision",
9931
- "valueType": "number"
9932
9985
  }
9933
9986
  ],
9934
9987
  "presence": [
@@ -9957,20 +10010,10 @@ Object.freeze({
9957
10010
  "valueType": "number"
9958
10011
  }
9959
10012
  ],
9960
- "pressure-sensor": [
9961
- {
9962
- "path": "hpa",
9963
- "valueType": "number"
9964
- },
9965
- {
9966
- "path": "unit",
9967
- "valueType": "string"
9968
- },
9969
- {
9970
- "path": "precision",
9971
- "valueType": "number"
9972
- }
9973
- ],
10013
+ "pressure-sensor": [{
10014
+ "path": "hpa",
10015
+ "valueType": "number"
10016
+ }],
9974
10017
  "privacy-mask": [{
9975
10018
  "path": "enabled",
9976
10019
  "valueType": "boolean"
@@ -10281,20 +10324,10 @@ Object.freeze({
10281
10324
  "path": "tampered",
10282
10325
  "valueType": "boolean"
10283
10326
  }],
10284
- "temperature-sensor": [
10285
- {
10286
- "path": "celsius",
10287
- "valueType": "number"
10288
- },
10289
- {
10290
- "path": "unit",
10291
- "valueType": "string"
10292
- },
10293
- {
10294
- "path": "precision",
10295
- "valueType": "number"
10296
- }
10297
- ],
10327
+ "temperature-sensor": [{
10328
+ "path": "celsius",
10329
+ "valueType": "number"
10330
+ }],
10298
10331
  "update": [
10299
10332
  {
10300
10333
  "path": "currentVersion",
@@ -10405,10 +10438,6 @@ Object.freeze({
10405
10438
  "path": "temperature",
10406
10439
  "valueType": "number"
10407
10440
  },
10408
- {
10409
- "path": "temperatureUnit",
10410
- "valueType": "string"
10411
- },
10412
10441
  {
10413
10442
  "path": "humidity",
10414
10443
  "valueType": "number"
@@ -10417,18 +10446,10 @@ Object.freeze({
10417
10446
  "path": "pressure",
10418
10447
  "valueType": "number"
10419
10448
  },
10420
- {
10421
- "path": "pressureUnit",
10422
- "valueType": "string"
10423
- },
10424
10449
  {
10425
10450
  "path": "windSpeed",
10426
10451
  "valueType": "number"
10427
10452
  },
10428
- {
10429
- "path": "windSpeedUnit",
10430
- "valueType": "string"
10431
- },
10432
10453
  {
10433
10454
  "path": "windBearing",
10434
10455
  "valueType": "number"
@@ -10845,7 +10866,7 @@ function errMsg(err) {
10845
10866
  return String(err);
10846
10867
  }
10847
10868
  //#endregion
10848
- //#region ../types/dist/composition-kEl9mevu.mjs
10869
+ //#region ../types/dist/composition-OhpuespA.mjs
10849
10870
  /**
10850
10871
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10851
10872
  *
@@ -11600,15 +11621,74 @@ function validateExpressionSource(src) {
11600
11621
  }
11601
11622
  return null;
11602
11623
  }
11603
- /** A composed device's stableId is this prefix plus the block id. */
11604
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11605
- /** Longest snippet a `code` source may carry (slice 4). */
11606
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11624
+ /**
11625
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11626
+ * id. Split out so helper schemas can name it without a cycle through
11627
+ * `composition.ts` (D659).
11628
+ */
11607
11629
  var CompositionSourceRefSchema = object({
11608
11630
  /** The owning addon; `stableId` is unique only within it. */
11609
11631
  addonId: string().min(1),
11610
11632
  stableId: string().min(1)
11611
11633
  });
11634
+ var GROUP_TEMPLATE_IDS = [
11635
+ "temperature",
11636
+ "humidity",
11637
+ "pressure",
11638
+ "illuminance",
11639
+ "power",
11640
+ "energy",
11641
+ "battery",
11642
+ "numeric",
11643
+ "motion",
11644
+ "contact",
11645
+ "leak",
11646
+ "smoke",
11647
+ "occupancy",
11648
+ "binary",
11649
+ "switch",
11650
+ "light"
11651
+ ];
11652
+ var NUMERIC_AGGREGATES = [
11653
+ "mean",
11654
+ "median",
11655
+ "min",
11656
+ "max",
11657
+ "sum"
11658
+ ];
11659
+ var BINARY_AGGREGATES = ["any", "all"];
11660
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11661
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11662
+ var AGGREGATE_REDUCERS = [
11663
+ "mean",
11664
+ "median",
11665
+ "min",
11666
+ "max",
11667
+ "sum",
11668
+ "any",
11669
+ "all"
11670
+ ];
11671
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11672
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11673
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11674
+ /** One member. The field is stored so occupancy can name motion or binary. */
11675
+ var HelperMemberSchema = object({
11676
+ source: CompositionSourceRefSchema,
11677
+ cap: string().min(1),
11678
+ fieldPath: string().min(1)
11679
+ });
11680
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11681
+ kind: literal("group"),
11682
+ templateId: GroupTemplateIdSchema,
11683
+ aggregate: AggregateReducerSchema,
11684
+ members: array(HelperMemberSchema).min(1).max(64),
11685
+ /** Actuator groups only. Readings leave this unset. */
11686
+ active: ActiveCriterionSchema.optional()
11687
+ })]);
11688
+ /** A composed device's stableId is this prefix plus the block id. */
11689
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11690
+ /** Longest snippet a `code` source may carry (slice 4). */
11691
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11612
11692
  /** One field of one capability of one source device. */
11613
11693
  var CompositionFieldReadSchema = object({
11614
11694
  source: CompositionSourceRefSchema,
@@ -11652,7 +11732,33 @@ var CompositionCodeSourceSchema = object({
11652
11732
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11653
11733
  CompositionFromSourceSchema,
11654
11734
  CompositionExpressionSourceSchema,
11655
- CompositionCodeSourceSchema
11735
+ CompositionCodeSourceSchema,
11736
+ object({
11737
+ kind: literal("aggregate"),
11738
+ reducer: _enum([
11739
+ "mean",
11740
+ "median",
11741
+ "min",
11742
+ "max",
11743
+ "sum",
11744
+ "any",
11745
+ "all"
11746
+ ]),
11747
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11748
+ /** `value` is the reading; the others are the member counts. */
11749
+ publish: _enum([
11750
+ "value",
11751
+ "included",
11752
+ "excluded",
11753
+ "total"
11754
+ ]),
11755
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11756
+ empty: _enum([
11757
+ "null",
11758
+ "omit",
11759
+ "offline"
11760
+ ])
11761
+ })
11656
11762
  ]);
11657
11763
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11658
11764
  kind: literal("forward"),
@@ -11698,7 +11804,13 @@ var CompositionSchema = object({
11698
11804
  kind: literal("existing"),
11699
11805
  device: CompositionSourceRefSchema
11700
11806
  })]),
11701
- features: array(CompositionFeatureSchema).min(1).max(16)
11807
+ features: array(CompositionFeatureSchema).min(1).max(16),
11808
+ /**
11809
+ * Present when the block is a helper template. Absent on a free
11810
+ * composition (the room example, device 4603). The editor reads this,
11811
+ * never the compiled features.
11812
+ */
11813
+ helper: HelperSpecSchema.optional()
11702
11814
  }).superRefine((composition, ctx) => {
11703
11815
  const seen = /* @__PURE__ */ new Set();
11704
11816
  composition.features.forEach((feature, index) => {
@@ -17906,7 +18018,11 @@ var CompositionProblemSchema = object({
17906
18018
  "item-field-reserved",
17907
18019
  "target-missing",
17908
18020
  "target-unreadable",
17909
- "not-replaceable"
18021
+ "not-replaceable",
18022
+ "native-capability-replace",
18023
+ "mixed-quantity",
18024
+ "helper-mismatch",
18025
+ "member-constraint"
17910
18026
  ]),
17911
18027
  path: string(),
17912
18028
  message: string()
@@ -17927,7 +18043,8 @@ var CompositionUnavailableModeSchema = _enum([
17927
18043
  "null",
17928
18044
  "offline",
17929
18045
  "unknown",
17930
- "release"
18046
+ "release",
18047
+ "omit"
17931
18048
  ]);
17932
18049
  /**
17933
18050
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17964,9 +18081,11 @@ var CompositionFieldPlanSchema = object({
17964
18081
  reads: array(CompositionFieldReadSchema)
17965
18082
  });
17966
18083
  /**
17967
- * What a feature does to its target (D663): `new` builds a capability on a new
17968
- * device; on an existing device it `add`s a capability the device lacks, or
17969
- * `replace`s named fields of one it has natively.
18084
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18085
+ * new device; on an existing device it `add`s a capability the device lacks.
18086
+ * Replacing a capability the device already serves is refused
18087
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18088
+ * wire for a claim the hub already holds.
17970
18089
  */
17971
18090
  var CompositionFeatureModeSchema = _enum([
17972
18091
  "new",
@@ -18139,6 +18258,275 @@ Object.freeze({
18139
18258
  result: ["number", "null"]
18140
18259
  }
18141
18260
  });
18261
+ /**
18262
+ * One catalog row per base reading a group helper can build (D944 §3).
18263
+ * Data, not a form default written into the block.
18264
+ */
18265
+ var NOW = {
18266
+ fieldPath: "lastFetchedAt",
18267
+ expr: "now"
18268
+ };
18269
+ var CHANGED = {
18270
+ fieldPath: "lastChangedAt",
18271
+ expr: "now"
18272
+ };
18273
+ function numeric(input) {
18274
+ return {
18275
+ ...input,
18276
+ family: "numeric",
18277
+ deviceType: DeviceType.Sensor,
18278
+ memberDeviceType: DeviceType.Sensor,
18279
+ commandCaps: [],
18280
+ baseUnit: baseUnitOf(input.quantity),
18281
+ aggregates: NUMERIC_AGGREGATES,
18282
+ accept: [{
18283
+ cap: input.outputCap,
18284
+ fieldPath: input.outputField
18285
+ }]
18286
+ };
18287
+ }
18288
+ function binary(input) {
18289
+ return {
18290
+ ...input,
18291
+ family: "binary",
18292
+ deviceType: DeviceType.Sensor,
18293
+ memberDeviceType: DeviceType.Sensor,
18294
+ commandCaps: [],
18295
+ quantity: "none",
18296
+ baseUnit: void 0,
18297
+ aggregates: BINARY_AGGREGATES,
18298
+ empty: "offline"
18299
+ };
18300
+ }
18301
+ var GROUP_TEMPLATES = [
18302
+ numeric({
18303
+ id: "temperature",
18304
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18305
+ role: DeviceRole.TemperatureSensor,
18306
+ outputCap: "temperature-sensor",
18307
+ outputField: "celsius",
18308
+ quantity: "temperature",
18309
+ defaultAggregate: "mean",
18310
+ empty: "null",
18311
+ companions: [NOW]
18312
+ }),
18313
+ numeric({
18314
+ id: "humidity",
18315
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18316
+ role: DeviceRole.HumiditySensor,
18317
+ outputCap: "humidity-sensor",
18318
+ outputField: "percent",
18319
+ quantity: "humidity",
18320
+ defaultAggregate: "mean",
18321
+ empty: "null",
18322
+ companions: [NOW]
18323
+ }),
18324
+ numeric({
18325
+ id: "pressure",
18326
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18327
+ role: DeviceRole.PressureSensor,
18328
+ outputCap: "pressure-sensor",
18329
+ outputField: "hpa",
18330
+ quantity: "pressure",
18331
+ defaultAggregate: "mean",
18332
+ empty: "null",
18333
+ companions: [NOW]
18334
+ }),
18335
+ numeric({
18336
+ id: "illuminance",
18337
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18338
+ role: DeviceRole.AmbientLightSensor,
18339
+ outputCap: "ambient-light-sensor",
18340
+ outputField: "lux",
18341
+ quantity: "illuminance",
18342
+ defaultAggregate: "mean",
18343
+ empty: "null",
18344
+ companions: [NOW]
18345
+ }),
18346
+ numeric({
18347
+ id: "power",
18348
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18349
+ role: DeviceRole.PowerSensor,
18350
+ outputCap: "power-meter",
18351
+ outputField: "watts",
18352
+ quantity: "power",
18353
+ defaultAggregate: "sum",
18354
+ empty: "omit",
18355
+ companions: [NOW]
18356
+ }),
18357
+ numeric({
18358
+ id: "energy",
18359
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18360
+ role: DeviceRole.EnergySensor,
18361
+ outputCap: "power-meter",
18362
+ outputField: "kwhTotal",
18363
+ quantity: "energy",
18364
+ defaultAggregate: "sum",
18365
+ empty: "omit",
18366
+ companions: [NOW]
18367
+ }),
18368
+ numeric({
18369
+ id: "battery",
18370
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18371
+ role: DeviceRole.BatterySensor,
18372
+ outputCap: "battery",
18373
+ outputField: "percentage",
18374
+ quantity: "percentage",
18375
+ defaultAggregate: "mean",
18376
+ empty: "null",
18377
+ companions: [
18378
+ {
18379
+ fieldPath: "charging",
18380
+ expr: "\"unknown\""
18381
+ },
18382
+ {
18383
+ fieldPath: "sleeping",
18384
+ expr: "false"
18385
+ },
18386
+ {
18387
+ fieldPath: "lastUpdated",
18388
+ expr: "now"
18389
+ }
18390
+ ]
18391
+ }),
18392
+ numeric({
18393
+ id: "numeric",
18394
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18395
+ role: DeviceRole.NumericSensor,
18396
+ outputCap: "numeric-sensor",
18397
+ outputField: "value",
18398
+ quantity: "device",
18399
+ defaultAggregate: "mean",
18400
+ empty: "null",
18401
+ companions: [NOW]
18402
+ }),
18403
+ binary({
18404
+ id: "motion",
18405
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18406
+ role: DeviceRole.MotionSensor,
18407
+ outputCap: "motion",
18408
+ outputField: "detected",
18409
+ defaultAggregate: "any",
18410
+ accept: [{
18411
+ cap: "motion",
18412
+ fieldPath: "detected"
18413
+ }],
18414
+ companions: [{
18415
+ fieldPath: "lastDetectedAt",
18416
+ expr: "null"
18417
+ }, {
18418
+ fieldPath: "autoClearAfterMs",
18419
+ expr: "null"
18420
+ }]
18421
+ }),
18422
+ binary({
18423
+ id: "contact",
18424
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18425
+ role: DeviceRole.ContactSensor,
18426
+ outputCap: "contact",
18427
+ outputField: "entryOpen",
18428
+ defaultAggregate: "any",
18429
+ accept: [{
18430
+ cap: "contact",
18431
+ fieldPath: "entryOpen"
18432
+ }],
18433
+ companions: [CHANGED]
18434
+ }),
18435
+ binary({
18436
+ id: "leak",
18437
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18438
+ role: DeviceRole.LeakSensor,
18439
+ outputCap: "flood",
18440
+ outputField: "flooded",
18441
+ defaultAggregate: "any",
18442
+ accept: [{
18443
+ cap: "flood",
18444
+ fieldPath: "flooded"
18445
+ }],
18446
+ companions: [CHANGED]
18447
+ }),
18448
+ binary({
18449
+ id: "smoke",
18450
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18451
+ role: DeviceRole.SmokeSensor,
18452
+ outputCap: "smoke",
18453
+ outputField: "detected",
18454
+ defaultAggregate: "any",
18455
+ accept: [{
18456
+ cap: "smoke",
18457
+ fieldPath: "detected"
18458
+ }],
18459
+ companions: [CHANGED]
18460
+ }),
18461
+ binary({
18462
+ id: "occupancy",
18463
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18464
+ role: DeviceRole.BinarySensor,
18465
+ outputCap: "binary",
18466
+ outputField: "on",
18467
+ defaultAggregate: "any",
18468
+ accept: [{
18469
+ cap: "motion",
18470
+ fieldPath: "detected"
18471
+ }, {
18472
+ cap: "binary",
18473
+ fieldPath: "on"
18474
+ }],
18475
+ companions: [CHANGED]
18476
+ }),
18477
+ binary({
18478
+ id: "binary",
18479
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18480
+ role: DeviceRole.BinarySensor,
18481
+ outputCap: "binary",
18482
+ outputField: "on",
18483
+ defaultAggregate: "any",
18484
+ accept: [{
18485
+ cap: "binary",
18486
+ fieldPath: "on"
18487
+ }],
18488
+ companions: [CHANGED]
18489
+ }),
18490
+ actuator({
18491
+ id: "switch",
18492
+ 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.",
18493
+ deviceType: DeviceType.Switch,
18494
+ outputCap: "switch",
18495
+ commandCaps: ["switch"]
18496
+ }),
18497
+ actuator({
18498
+ id: "light",
18499
+ 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.",
18500
+ deviceType: DeviceType.Light,
18501
+ outputCap: "switch",
18502
+ commandCaps: [
18503
+ "switch",
18504
+ "brightness",
18505
+ "color"
18506
+ ]
18507
+ })
18508
+ ];
18509
+ function actuator(input) {
18510
+ return {
18511
+ ...input,
18512
+ family: "actuator",
18513
+ role: void 0,
18514
+ memberDeviceType: input.deviceType,
18515
+ outputField: "on",
18516
+ quantity: "none",
18517
+ baseUnit: void 0,
18518
+ aggregates: BINARY_AGGREGATES,
18519
+ defaultAggregate: "any",
18520
+ empty: "offline",
18521
+ accept: [{
18522
+ cap: "switch",
18523
+ fieldPath: "on"
18524
+ }],
18525
+ companions: [CHANGED]
18526
+ };
18527
+ }
18528
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18529
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18142
18530
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18143
18531
  /**
18144
18532
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19852,12 +20240,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19852
20240
  * providers detect renames by matching `uniqueId` and patch `id`
19853
20241
  * in-place so dispatch keeps working.
19854
20242
  *
19855
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19856
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19857
- * state push and are the single source of truth. The `device_class` tag is
19858
- * an HA-specific input that is consumed at the discovery boundary
19859
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19860
- * stored after that point.
20243
+ * **Display unit and precision are NOT stored here.** They are a read-time
20244
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20245
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20246
+ * neither is repeated on each sample. The `device_class` tag is an
20247
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20248
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19861
20249
  *
19862
20250
  * The `raw` escape hatch carries provider-specific attributes that don't
19863
20251
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19871,6 +20259,14 @@ var SourceInfoSchema = object({
19871
20259
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19872
20260
  * Used to detect rename when `id` changes. */
19873
20261
  uniqueId: string().optional(),
20262
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20263
+ quantity: ReadingQuantitySchema.optional(),
20264
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20265
+ dateTimeShape: _enum([
20266
+ "date",
20267
+ "time",
20268
+ "datetime"
20269
+ ]).optional(),
19874
20270
  /** Provider-specific extras that don't fit the structured slots.
19875
20271
  * Whitelist what goes here per provider — do NOT dump entire HA
19876
20272
  * attribute blobs (would explode the meta JSON column). */
@@ -19916,6 +20312,80 @@ function mergeSourceInfo(prev, patch) {
19916
20312
  return next;
19917
20313
  }
19918
20314
  /**
20315
+ * Coerce common unit-of-measurement string variants to a canonical form
20316
+ * so the UI's display templates can rely on stable strings. Pure
20317
+ * passthrough for unknown units — kept conservative on purpose, the goal
20318
+ * is to normalise the long tail of HA `unit_of_measurement` values that
20319
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
20320
+ *
20321
+ * Extend the map as we encounter real-world data. Returning the raw
20322
+ * input for unknown values is intentional: providers should never
20323
+ * lose information silently here.
20324
+ */
20325
+ function normalizeUnit(raw) {
20326
+ if (raw == null) return void 0;
20327
+ const trimmed = raw.trim();
20328
+ if (trimmed === "") return void 0;
20329
+ const lower = trimmed.toLowerCase();
20330
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
20331
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
20332
+ if (lower === "k" || lower === "kelvin") return "K";
20333
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
20334
+ if (lower === "lx" || lower === "lux") return "lx";
20335
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
20336
+ if (lower === "kpa") return "kPa";
20337
+ if (lower === "pa") return "Pa";
20338
+ if (lower === "mbar" || lower === "millibar") return "mbar";
20339
+ if (lower === "w" || lower === "watt") return "W";
20340
+ if (lower === "kw" || lower === "kilowatt") return "kW";
20341
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
20342
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
20343
+ if (lower === "v" || lower === "volt") return "V";
20344
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
20345
+ if (lower === "ma" || lower === "milliampere") return "mA";
20346
+ if (lower === "va") return "VA";
20347
+ if (lower === "ppm") return "ppm";
20348
+ if (lower === "ppb") return "ppb";
20349
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
20350
+ if (lower === "km/h" || lower === "kph") return "km/h";
20351
+ if (lower === "mph") return "mph";
20352
+ if (lower === "m/s") return "m/s";
20353
+ if (lower === "m") return "m";
20354
+ if (lower === "m²" || lower === "m2") return "m²";
20355
+ if (lower === "mm") return "mm";
20356
+ if (lower === "cm") return "cm";
20357
+ if (lower === "km") return "km";
20358
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
20359
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
20360
+ if (lower === "kg") return "kg";
20361
+ if (lower === "g") return "g";
20362
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
20363
+ if (lower === "ml") return "mL";
20364
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
20365
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
20366
+ if (lower === "ms") return "ms";
20367
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
20368
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
20369
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
20370
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
20371
+ if (lower === "kib") return "KiB";
20372
+ if (lower === "mib") return "MiB";
20373
+ if (lower === "gib") return "GiB";
20374
+ if (lower === "tib") return "TiB";
20375
+ if (lower === "kb") return "kB";
20376
+ if (lower === "mb") return "MB";
20377
+ if (lower === "gb") return "GB";
20378
+ if (lower === "dbm") return "dBm";
20379
+ if (lower === "dbfs") return "dBFS";
20380
+ if (lower === "inhg") return "inHg";
20381
+ if (lower === "mm/h") return "mm/h";
20382
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
20383
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
20384
+ if (lower === "kbps") return "kbps";
20385
+ if (lower === "fps") return "fps";
20386
+ return trimmed;
20387
+ }
20388
+ /**
19919
20389
  * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
19920
20390
  * blob is a free-form `Record<string, unknown>` shared with hardware-
19921
20391
  * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
@@ -40992,7 +41462,8 @@ method(object({
40992
41462
  "enum"
40993
41463
  ]),
40994
41464
  enumValues: array(string()).optional(),
40995
- item: boolean().optional()
41465
+ item: boolean().optional(),
41466
+ quantity: ReadingQuantitySchema.optional()
40996
41467
  })).readonly(),
40997
41468
  itemArray: object({
40998
41469
  path: string(),
@@ -44848,8 +45319,8 @@ var MaskPolygonShapeSchema = object({
44848
45319
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44849
45320
  var MaskGridShapeSchema = object({
44850
45321
  kind: literal("grid"),
44851
- gridWidth: number(),
44852
- gridHeight: number(),
45322
+ gridWidth: number().meta(readingMeta("none")),
45323
+ gridHeight: number().meta(readingMeta("none")),
44853
45324
  cells: array(boolean())
44854
45325
  });
44855
45326
  discriminatedUnion("kind", [
@@ -44961,6 +45432,8 @@ var NcSystemEventKindSchema = _enum([
44961
45432
  "detection-blind",
44962
45433
  "motion-noisy",
44963
45434
  "motion-calm",
45435
+ "package-llm-silent",
45436
+ "package-llm-answered",
44964
45437
  "recording-stalled",
44965
45438
  "recording-healthy",
44966
45439
  "recording-degraded",
@@ -56052,6 +56525,28 @@ var PackageVersionInfoSchema = object({
56052
56525
  deprecated: string().optional(),
56053
56526
  distTags: array(string()).readonly()
56054
56527
  });
56528
+ /**
56529
+ * One published document. `ok` is markdown from the registry. `missing` means
56530
+ * the registry answered and the file was not there. `error` is a registry or
56531
+ * tarball failure — never an empty string standing in for either.
56532
+ */
56533
+ var PackageDocBodySchema = discriminatedUnion("status", [
56534
+ object({
56535
+ status: literal("ok"),
56536
+ markdown: string()
56537
+ }),
56538
+ object({ status: literal("missing") }),
56539
+ object({
56540
+ status: literal("error"),
56541
+ message: string()
56542
+ })
56543
+ ]);
56544
+ var PackageDocsSchema = object({
56545
+ name: string(),
56546
+ version: string(),
56547
+ readme: PackageDocBodySchema,
56548
+ changelog: PackageDocBodySchema
56549
+ });
56055
56550
  var SearchResultSchema = object({
56056
56551
  name: string(),
56057
56552
  version: string(),
@@ -56270,7 +56765,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56270
56765
  }), object({ success: literal(true) }), {
56271
56766
  kind: "mutation",
56272
56767
  auth: "admin"
56273
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56768
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56769
+ name: string().min(1),
56770
+ version: string().min(1).optional()
56771
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56274
56772
  kind: "mutation",
56275
56773
  auth: "admin"
56276
56774
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56329,27 +56827,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56329
56827
  */
56330
56828
  var AirQualitySensorStatusSchema = object({
56331
56829
  /** Carbon dioxide concentration in ppm. */
56332
- co2Ppm: number().min(0).optional(),
56830
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56333
56831
  /** Total volatile organic compounds in ppb. */
56334
- vocPpb: number().min(0).optional(),
56832
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56335
56833
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56336
- pm25: number().min(0).optional(),
56834
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56337
56835
  /** Particulate matter ≤ 10 μm in µg/m³. */
56338
- pm10: number().min(0).optional(),
56836
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56339
56837
  /** Composite AQI value (typically 0..500). */
56340
- aqi: number().optional(),
56838
+ aqi: number().optional().meta(readingMeta("index")),
56341
56839
  /** Ms epoch when the slice was last updated. */
56342
- lastFetchedAt: number(),
56343
- /** Live display unit of the single metric this slice carries (e.g. HA
56344
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56345
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56346
- * per slice is unambiguous. */
56347
- unit: string().optional(),
56348
- /** Suggested decimal places for numeric display.
56349
- * Populated live from the upstream source when provided (e.g. HA
56350
- * `attributes.suggested_display_precision`). Falls back to
56351
- * auto-formatting when absent. */
56352
- precision: number().int().min(0).max(10).optional()
56840
+ lastFetchedAt: number()
56353
56841
  });
56354
56842
  var airQualitySensorCapability = {
56355
56843
  name: "air-quality-sensor",
@@ -56386,12 +56874,12 @@ var airQualitySensorCapability = {
56386
56874
  },
56387
56875
  pm25: {
56388
56876
  kind: "gauge",
56389
- unit: "ug/m3",
56877
+ unit: "µg/m³",
56390
56878
  deadband: 1
56391
56879
  },
56392
56880
  pm10: {
56393
56881
  kind: "gauge",
56394
- unit: "ug/m3",
56882
+ unit: "µg/m³",
56395
56883
  deadband: 1
56396
56884
  },
56397
56885
  aqi: {
@@ -56409,22 +56897,13 @@ var airQualitySensorCapability = {
56409
56897
  * entries with `device_class: illuminance`.
56410
56898
  */
56411
56899
  var AmbientLightSensorStatusSchema = object({
56412
- /** Current illuminance in lux (lx). */
56413
- lux: number().min(0).meta({
56414
- unit: "lx",
56900
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56901
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56902
+ precision: 0,
56415
56903
  label: "Illuminance"
56416
- }),
56904
+ })),
56417
56905
  /** Ms epoch when the slice was last updated. */
56418
- lastFetchedAt: number(),
56419
- /** Live display unit from the upstream source (e.g. HA
56420
- * `attributes.unit_of_measurement`). The UI prefers this over the
56421
- * role's canonical unit. Absent → fall back to the canonical unit. */
56422
- unit: string().optional(),
56423
- /** Suggested decimal places for numeric display.
56424
- * Populated live from the upstream source when provided (e.g. HA
56425
- * `attributes.suggested_display_precision`). Falls back to
56426
- * auto-formatting when absent. */
56427
- precision: number().int().min(0).max(10).optional()
56906
+ lastFetchedAt: number()
56428
56907
  });
56429
56908
  var ambientLightSensorCapability = {
56430
56909
  name: "ambient-light-sensor",
@@ -56456,12 +56935,7 @@ var ambientLightSensorCapability = {
56456
56935
  } } },
56457
56936
  /** Clock fields: written, but excluded from the compare that decides
56458
56937
  * whether persisting is worth a SQLite commit. */
56459
- volatileStateFields: ["lastFetchedAt"],
56460
- customize: {
56461
- version: 1,
56462
- advanced: "derive",
56463
- targets: []
56464
- }
56938
+ volatileStateFields: ["lastFetchedAt"]
56465
56939
  };
56466
56940
  /**
56467
56941
  * Per-class audio metrics aggregated over a sliding window.
@@ -56688,10 +57162,7 @@ var BatteryStatusSchema = object({
56688
57162
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56689
57163
  * declines to band a non-finite reading, which is the correct shape.
56690
57164
  */
56691
- percentage: number().min(0).max(100).nullable().meta({
56692
- unit: "%",
56693
- label: "Charge"
56694
- }),
57165
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56695
57166
  /**
56696
57167
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56697
57168
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56718,7 +57189,14 @@ var BatteryStatusSchema = object({
56718
57189
  */
56719
57190
  sleeping: boolean().meta({ label: "Sleeping" }),
56720
57191
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56721
- lastUpdated: number(),
57192
+ lastUpdated: number().meta(readingMeta("none")),
57193
+ /**
57194
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57195
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57196
+ * that only report charge. A second reading on this cap, not a second
57197
+ * `numeric-sensor` on the same device.
57198
+ */
57199
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56722
57200
  /**
56723
57201
  * Ms epoch of the last time the device PROVED it was reachable — a
56724
57202
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56770,7 +57248,6 @@ var BATTERY_DEVICE_TYPES = [
56770
57248
  /** Generic Customize target for the percentage reading (D936). */
56771
57249
  var batteryCustomize = {
56772
57250
  version: 1,
56773
- advanced: "derive",
56774
57251
  targets: [{
56775
57252
  id: "percentage",
56776
57253
  cardinality: "scalar",
@@ -56779,7 +57256,7 @@ var batteryCustomize = {
56779
57256
  stateField: "percentage"
56780
57257
  },
56781
57258
  deviceTypes: BATTERY_DEVICE_TYPES,
56782
- apply: "add-or-replace",
57259
+ apply: "add",
56783
57260
  path: "percentage",
56784
57261
  sources: [{
56785
57262
  kind: "field",
@@ -56900,10 +57377,16 @@ onStatusChanged: { data: object({
56900
57377
  /**
56901
57378
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56902
57379
  */
56903
- history: { fields: { percentage: {
56904
- kind: "gauge",
56905
- unit: "%"
56906
- } } },
57380
+ history: { fields: {
57381
+ percentage: {
57382
+ kind: "gauge",
57383
+ unit: "%"
57384
+ },
57385
+ volts: {
57386
+ kind: "gauge",
57387
+ unit: "V"
57388
+ }
57389
+ } },
56907
57390
  customize: batteryCustomize,
56908
57391
  /** Clock fields: written, but excluded from the compare that decides
56909
57392
  * whether persisting is worth a SQLite commit. */
@@ -56950,12 +57433,7 @@ var binaryCapability = {
56950
57433
  history: { fields: { on: { kind: "boolean" } } },
56951
57434
  /** Clock fields: written, but excluded from the compare that decides
56952
57435
  * whether persisting is worth a SQLite commit. */
56953
- volatileStateFields: ["lastChangedAt"],
56954
- customize: {
56955
- version: 1,
56956
- advanced: "derive",
56957
- targets: []
56958
- }
57436
+ volatileStateFields: ["lastChangedAt"]
56959
57437
  };
56960
57438
  /**
56961
57439
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -56971,7 +57449,7 @@ var binaryCapability = {
56971
57449
  */
56972
57450
  var BrightnessStatusSchema = object({
56973
57451
  /** Current level as 0..100 inclusive. Firmware-reported. */
56974
- percentage: number().min(0).max(100),
57452
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56975
57453
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56976
57454
  lastChangedAt: number()
56977
57455
  });
@@ -57442,21 +57920,21 @@ var ClimateControlStatusSchema = object({
57442
57920
  /** Single setpoint in Celsius. Used by single-target modes
57443
57921
  * (heat / cool / dry). Null when the device is in a range mode
57444
57922
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57445
- target: number().nullable(),
57923
+ target: number().nullable().meta(readingMeta("temperature")),
57446
57924
  /** Upper bound of the dual setpoint in Celsius. Populated only
57447
57925
  * in `heat_cool` mode. */
57448
- targetHigh: number().nullable(),
57926
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57449
57927
  /** Lower bound of the dual setpoint in Celsius. Populated only
57450
57928
  * in `heat_cool` mode. */
57451
- targetLow: number().nullable(),
57929
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57452
57930
  /** Target relative humidity (0..100). Null when the device has no
57453
57931
  * humidity control. */
57454
- targetHumidity: number().min(0).max(100).nullable(),
57932
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57455
57933
  /** Current measured temperature in Celsius. Read-only — pushed by
57456
57934
  * the device's internal sensor. */
57457
- currentTemp: number().nullable(),
57935
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57458
57936
  /** Current measured relative humidity (0..100). Read-only. */
57459
- currentHumidity: number().min(0).max(100).nullable(),
57937
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57460
57938
  /** Vertical louver swing on/off. Null when the device has no vertical
57461
57939
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57462
57940
  swingVertical: boolean().nullable(),
@@ -57464,11 +57942,11 @@ var ClimateControlStatusSchema = object({
57464
57942
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57465
57943
  swingHorizontal: boolean().nullable(),
57466
57944
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57467
- minTemp: number().nullable().optional(),
57945
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57468
57946
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57469
- maxTemp: number().nullable().optional(),
57947
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57470
57948
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57471
- tempStep: number().positive().nullable().optional(),
57949
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57472
57950
  /** Ms epoch when the slice was last updated (push or command). */
57473
57951
  lastFetchedAt: number()
57474
57952
  });
@@ -57610,17 +58088,17 @@ var climateControlCapability = {
57610
58088
  * modes to expose in the picker without a slice fetch.
57611
58089
  */
57612
58090
  var RgbTripletSchema = object({
57613
- r: number().int().min(0).max(255),
57614
- g: number().int().min(0).max(255),
57615
- b: number().int().min(0).max(255)
58091
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58092
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58093
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57616
58094
  });
57617
58095
  var HsvTripletSchema = object({
57618
58096
  /** Hue in degrees, 0..360. */
57619
- h: number().min(0).max(360),
58097
+ h: number().min(0).max(360).meta(readingMeta("none")),
57620
58098
  /** Saturation as 0..100 inclusive. */
57621
- s: number().min(0).max(100),
58099
+ s: number().min(0).max(100).meta(readingMeta("none")),
57622
58100
  /** Value/brightness as 0..100 inclusive. */
57623
- v: number().min(0).max(100)
58101
+ v: number().min(0).max(100).meta(readingMeta("none"))
57624
58102
  });
57625
58103
  var ColorInputSchema = discriminatedUnion("mode", [
57626
58104
  object({
@@ -57649,7 +58127,7 @@ var ColorStatusSchema = object({
57649
58127
  /** Populated when `mode === 'hsv'`. */
57650
58128
  hsv: HsvTripletSchema.optional(),
57651
58129
  /** Populated when `mode === 'mired'`. */
57652
- mireds: number().int().optional(),
58130
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57653
58131
  /** Ms epoch of the last operator-driven change. */
57654
58132
  lastChangedAt: number()
57655
58133
  });
@@ -57856,18 +58334,12 @@ _enum([
57856
58334
  function consumableKindOf(item) {
57857
58335
  return item.kind ?? "wear";
57858
58336
  }
57859
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57860
- * each. */
57861
- var ConsumableRemainingUnitSchema = _enum([
57862
- "days",
57863
- "hours",
57864
- "cycles"
57865
- ]);
57866
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57867
- * days). `value` null = unknown (D393). */
58337
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58338
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58339
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57868
58340
  var ConsumableRemainingSchema = object({
57869
- value: number().min(0).nullable(),
57870
- unit: ConsumableRemainingUnitSchema
58341
+ value: number().min(0).nullable().meta(readingMeta("device")),
58342
+ kind: ConsumableRemainingKindSchema
57871
58343
  });
57872
58344
  /** A single consumable item. Either a continuous `level` (remaining
57873
58345
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57880,7 +58352,7 @@ var ConsumableItemSchema = object({
57880
58352
  /** Display name. */
57881
58353
  label: string().min(1),
57882
58354
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57883
- level: number().min(0).max(100).nullable(),
58355
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57884
58356
  /** The provider's discrete verdict when known (binary mode). */
57885
58357
  status: ConsumableStatusSchema.nullable(),
57886
58358
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57962,8 +58434,8 @@ var consumablesCustomize = {
57962
58434
  transforms: [{ kind: "identity" }]
57963
58435
  }],
57964
58436
  literals: [{
57965
- path: "remaining.unit",
57966
- values: [...ConsumableRemainingUnitSchema.options]
58437
+ path: "remaining.kind",
58438
+ values: [...ConsumableRemainingKindSchema.options]
57967
58439
  }]
57968
58440
  },
57969
58441
  {
@@ -58092,7 +58564,6 @@ reset: method(object({
58092
58564
  },
58093
58565
  "remaining.value": {
58094
58566
  kind: "gauge",
58095
- unitFrom: "remaining.unit",
58096
58567
  deadband: 1,
58097
58568
  displayName: "Remaining"
58098
58569
  }
@@ -58162,10 +58633,9 @@ var contactCapability = {
58162
58633
  * (empty array for the other kinds). It lives in the slice because
58163
58634
  * HA `select.*` entities may update their options at runtime.
58164
58635
  *
58165
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58166
- * `kind === 'numeric'` and are absent (undefined) for all other
58167
- * kinds. They are populated live from HA attributes on each state
58168
- * push — consistent with `options` and the sensor-unit approach.
58636
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58637
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58638
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58169
58639
  */
58170
58640
  var ControlStatusSchema = object({
58171
58641
  kind: _enum([
@@ -58177,39 +58647,17 @@ var ControlStatusSchema = object({
58177
58647
  /** Current value. Numeric kind → number; select/text/datetime → string.
58178
58648
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58179
58649
  * or time-only — HA accepts all three forms). */
58180
- value: union([number(), string()]),
58650
+ value: union([number().meta(readingMeta("device")), string()]),
58181
58651
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58182
58652
  options: array(string()),
58183
58653
  /** Ms epoch when the slice was last updated. */
58184
58654
  lastChangedAt: number(),
58185
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58186
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58187
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58188
- unit: string().optional(),
58189
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58190
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58191
- min: number().optional(),
58192
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58193
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58194
- max: number().optional(),
58195
- /** Step increment. Populated live from HA `attributes.step`.
58196
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58197
- step: number().optional(),
58198
- /** Suggested decimal places for numeric display. Populated live from HA
58199
- * `attributes.suggested_display_precision`, or derived from `step` when the
58200
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58201
- * other kinds. Falls back to auto-formatting when absent. */
58202
- precision: number().int().min(0).max(10).optional(),
58203
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58204
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58205
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58206
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58207
- * `has_date`/`has_time` attributes on each push. */
58208
- format: _enum([
58209
- "date",
58210
- "time",
58211
- "datetime"
58212
- ]).optional()
58655
+ /** Minimum allowed value, in the device quantity's base unit. */
58656
+ min: number().optional().meta(readingMeta("device")),
58657
+ /** Maximum allowed value, in the device quantity's base unit. */
58658
+ max: number().optional().meta(readingMeta("device")),
58659
+ /** Step increment, in the device quantity's base unit. */
58660
+ step: number().optional().meta(readingMeta("device"))
58213
58661
  });
58214
58662
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58215
58663
  object({
@@ -58275,9 +58723,9 @@ var CoverStatusSchema = object({
58275
58723
  ]),
58276
58724
  /** 0 = fully closed, 100 = fully open. Null when the device has
58277
58725
  * no intermediate position surface. */
58278
- position: number().min(0).max(100).nullable(),
58726
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58279
58727
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58280
- tiltPosition: number().min(0).max(100).nullable(),
58728
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58281
58729
  /** Ms epoch when the slice was last updated. */
58282
58730
  lastChangedAt: number()
58283
58731
  });
@@ -58380,9 +58828,9 @@ var NormalizedRangeSchema$1 = object({
58380
58828
  var DayNightStatusSchema = object({
58381
58829
  mode: DayNightModeSchema,
58382
58830
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58383
- sensitivity: number().optional(),
58831
+ sensitivity: number().optional().meta(readingMeta("none")),
58384
58832
  /** Delay before the IR-cut filter flips, in seconds. */
58385
- switchDelaySec: number().optional(),
58833
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58386
58834
  lastFetchedAt: number()
58387
58835
  });
58388
58836
  /**
@@ -58600,11 +59048,11 @@ var DishwasherStatusSchema = object({
58600
59048
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58601
59049
  availablePrograms: array(string()),
58602
59050
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58603
- remainingSeconds: number().int().nonnegative().nullable(),
59051
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58604
59052
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58605
- delayedStartSeconds: number().int().nonnegative().nullable(),
59053
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58606
59054
  /** Cycle completion 0..100, or null when not applicable. */
58607
- progressPercent: number().min(0).max(100).nullable(),
59055
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58608
59056
  /** True when the door is reported open; null when the door state is not known. */
58609
59057
  doorOpen: boolean().nullable(),
58610
59058
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58628,7 +59076,7 @@ var DishwasherStatusSchema = object({
58628
59076
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58629
59077
  machineCareDue: boolean().nullable(),
58630
59078
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58631
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59079
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58632
59080
  /** Option states: null when the appliance does not expose the option or has not said. */
58633
59081
  halfLoad: boolean().nullable(),
58634
59082
  hygienePlus: boolean().nullable(),
@@ -58643,7 +59091,7 @@ var DishwasherStatusSchema = object({
58643
59091
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58644
59092
  activeEvents: array(string()),
58645
59093
  /** Numeric error code when the appliance reports one; null otherwise. */
58646
- errorCode: number().nullable(),
59094
+ errorCode: number().nullable().meta(readingMeta("none")),
58647
59095
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58648
59096
  errorLabel: string().nullable(),
58649
59097
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58874,7 +59322,7 @@ var DoorbellStatusSchema = object({
58874
59322
  /** Ms epoch of the last press. null = never observed since this provider started. */
58875
59323
  lastPressedAt: number().nullable(),
58876
59324
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58877
- pressCountSinceStart: number()
59325
+ pressCountSinceStart: number().meta(readingMeta("count"))
58878
59326
  });
58879
59327
  var DoorbellPressEventSchema = object({
58880
59328
  deviceId: number(),
@@ -58939,17 +59387,7 @@ onPressed: {
58939
59387
  history: "none",
58940
59388
  customize: doorbellCustomize
58941
59389
  };
58942
- /**
58943
- * Enum-state sensor — a string value picked from a finite option set.
58944
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58945
- * states, weather conditions, contact-source channels, mode strings).
58946
- * The option set is stable across observations; it is persisted in the
58947
- * device config blob (via the control slice at adoption) and is NOT
58948
- * stored in `sourceInfo`.
58949
- */
58950
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58951
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58952
- var EnumSensorDateTimeFormatSchema = _enum([
59390
+ _enum([
58953
59391
  "date",
58954
59392
  "time",
58955
59393
  "datetime"
@@ -58957,12 +59395,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58957
59395
  var EnumSensorStatusSchema = object({
58958
59396
  value: string(),
58959
59397
  /**
58960
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58961
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58962
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58963
- * a time-only sensor → `'time'`.
59398
+ * Set when `value` is a timestamp rather than an enum state, so history
59399
+ * does not learn ISO strings as states. The display shape (date / time /
59400
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58964
59401
  */
58965
- format: EnumSensorDateTimeFormatSchema.optional(),
59402
+ timestamp: boolean().optional(),
58966
59403
  /** Ms epoch when the slice was last updated. */
58967
59404
  lastFetchedAt: number()
58968
59405
  });
@@ -58989,11 +59426,11 @@ var enumSensorCapability = {
58989
59426
  * History (D850): the value as an `enum` series (an appliance's program or
58990
59427
  * operation state, an HVAC action). The option set is per device, so the
58991
59428
  * value list is LEARNED as values are first seen (bounded). A sensor with a
58992
- * `format` carries a timestamp, not a state, and is not recorded.
59429
+ * `timestamp` carries a clock, not a state, and is not recorded.
58993
59430
  */
58994
59431
  history: { fields: { value: {
58995
59432
  kind: "enum",
58996
- skipWhenSet: "format"
59433
+ skipWhenSet: "timestamp"
58997
59434
  } } },
58998
59435
  /** Clock fields: written, but excluded from the compare that decides
58999
59436
  * whether persisting is worth a SQLite commit. */
@@ -59013,13 +59450,13 @@ var EventFireSchema = object({
59013
59450
  deviceId: number(),
59014
59451
  eventType: string(),
59015
59452
  data: record(string(), unknown()).nullable(),
59016
- timestamp: number(),
59017
- seq: number()
59453
+ timestamp: number().meta(readingMeta("none")),
59454
+ seq: number().meta(readingMeta("count"))
59018
59455
  });
59019
59456
  var EventEmitterStatusSchema = object({
59020
59457
  eventTypes: array(string()),
59021
59458
  lastEvent: EventFireSchema.nullable(),
59022
- eventCountSinceStart: number()
59459
+ eventCountSinceStart: number().meta(readingMeta("count"))
59023
59460
  });
59024
59461
  var eventEmitterCapability = {
59025
59462
  name: "event-emitter",
@@ -59432,10 +59869,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59432
59869
  var FanControlStatusSchema = object({
59433
59870
  /** Active speed as 0..100 inclusive. Null when the device has no
59434
59871
  * speed surface (single-speed fan). */
59435
- percentage: number().min(0).max(100).nullable(),
59872
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59436
59873
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59437
59874
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59438
- percentageStep: number().positive().optional(),
59875
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59439
59876
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59440
59877
  * — empty string when no preset is active or supported. */
59441
59878
  preset: string(),
@@ -59556,7 +59993,7 @@ var FeatureProbeStatusSchema = object({
59556
59993
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59557
59994
  model: string().nullable(),
59558
59995
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59559
- channelCount: number().nullable(),
59996
+ channelCount: number().nullable().meta(readingMeta("count")),
59560
59997
  /**
59561
59998
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59562
59999
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59679,9 +60116,91 @@ var gasCapability = {
59679
60116
  * whether persisting is worth a SQLite commit. */
59680
60117
  volatileStateFields: ["lastChangedAt"]
59681
60118
  };
60119
+ var GroupCommandsSchema = object({
60120
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60121
+ results: array(object({
60122
+ deviceId: number().int().nonnegative(),
60123
+ cap: string().min(1),
60124
+ method: string().min(1),
60125
+ ok: boolean(),
60126
+ error: string().optional(),
60127
+ /** Ms epoch of the attempt. */
60128
+ at: number()
60129
+ })) });
60130
+ var groupCommandsCapability = {
60131
+ name: "group-commands",
60132
+ scope: "device",
60133
+ deviceNative: true,
60134
+ mode: "singleton",
60135
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60136
+ methods: {},
60137
+ status: {
60138
+ schema: GroupCommandsSchema,
60139
+ kind: "push"
60140
+ },
60141
+ runtimeState: GroupCommandsSchema,
60142
+ /**
60143
+ * Runtime-state durability: **session** — the last attempt. A restored
60144
+ * failure would outlive the command that caused it.
60145
+ *
60146
+ * See `RuntimeStateDurability`. Enforced by
60147
+ * `scripts/check-runtime-state-durability.ts`.
60148
+ */
60149
+ durability: "session",
60150
+ history: "none"
60151
+ };
60152
+ /**
60153
+ * How many members of a helper group answered (D944). Counts are integers,
60154
+ * not measurements: zero included is a real count, never a stand-in reading.
60155
+ * The reading itself stays on the output capability. History is none — the
60156
+ * series that later statistics read is the output cap's own history block.
60157
+ */
60158
+ var GroupStatusSchema = object({
60159
+ /** Members whose current value entered the aggregate. */
60160
+ included: number().int().nonnegative().meta(readingMeta("count", {
60161
+ precision: 0,
60162
+ label: "Reporting"
60163
+ })),
60164
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60165
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60166
+ precision: 0,
60167
+ label: "Excluded"
60168
+ })),
60169
+ /** `included` + `excluded`. */
60170
+ total: number().int().nonnegative().meta(readingMeta("count", {
60171
+ precision: 0,
60172
+ label: "Members"
60173
+ }))
60174
+ });
60175
+ var groupStatusCapability = {
60176
+ name: "group-status",
60177
+ scope: "device",
60178
+ deviceNative: true,
60179
+ mode: "singleton",
60180
+ deviceTypes: [
60181
+ DeviceType.Sensor,
60182
+ DeviceType.Switch,
60183
+ DeviceType.Light
60184
+ ],
60185
+ methods: {},
60186
+ status: {
60187
+ schema: GroupStatusSchema,
60188
+ kind: "push"
60189
+ },
60190
+ runtimeState: GroupStatusSchema,
60191
+ /**
60192
+ * Runtime-state durability: **session** — recomputed from members on the
60193
+ * next source event; a restored count would outlive the readings it named.
60194
+ *
60195
+ * See `RuntimeStateDurability`. Enforced by
60196
+ * `scripts/check-runtime-state-durability.ts`.
60197
+ */
60198
+ durability: "session",
60199
+ history: "none"
60200
+ };
59682
60201
  var HeatingValveStatusSchema = object({
59683
60202
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59684
- position: number().min(0).max(100).nullable(),
60203
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59685
60204
  /** Ms epoch when the slice was last updated (push or poll). */
59686
60205
  lastFetchedAt: number()
59687
60206
  });
@@ -59724,9 +60243,9 @@ var HumidifierStatusSchema = object({
59724
60243
  /** Whether the humidifier is currently on. */
59725
60244
  on: boolean(),
59726
60245
  /** Current measured relative humidity (0..100). Null when not reported. */
59727
- currentHumidity: number().min(0).max(100).nullable(),
60246
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59728
60247
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59729
- targetHumidity: number().min(0).max(100).nullable(),
60248
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59730
60249
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59731
60250
  * device has no mode surface. */
59732
60251
  mode: string().nullable(),
@@ -59736,9 +60255,9 @@ var HumidifierStatusSchema = object({
59736
60255
  * `idle` / `off`). Null when the device doesn't report it. */
59737
60256
  action: string().nullable(),
59738
60257
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59739
- minHumidity: number().nullable(),
60258
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59740
60259
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59741
- maxHumidity: number().nullable(),
60260
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59742
60261
  /** Ms epoch when the slice was last updated. */
59743
60262
  lastChangedAt: number()
59744
60263
  });
@@ -59808,22 +60327,13 @@ var humidifierCapability = {
59808
60327
  * `sourceInfo`.
59809
60328
  */
59810
60329
  var HumiditySensorStatusSchema = object({
59811
- /** Current relative humidity, 0..100. */
59812
- percent: number().min(0).max(100).meta({
59813
- unit: "%",
60330
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60331
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60332
+ precision: 0,
59814
60333
  label: "Humidity"
59815
- }),
60334
+ })),
59816
60335
  /** Ms epoch when the slice was last updated. */
59817
- lastFetchedAt: number(),
59818
- /** Live display unit from the upstream source (e.g. HA
59819
- * `attributes.unit_of_measurement`). The UI prefers this over the
59820
- * role's canonical unit. Absent → fall back to the canonical unit. */
59821
- unit: string().optional(),
59822
- /** Suggested decimal places for numeric display.
59823
- * Populated live from the upstream source when provided (e.g. HA
59824
- * `attributes.suggested_display_precision`). Falls back to
59825
- * auto-formatting when absent. */
59826
- precision: number().int().min(0).max(10).optional()
60336
+ lastFetchedAt: number()
59827
60337
  });
59828
60338
  var humiditySensorCapability = {
59829
60339
  name: "humidity-sensor",
@@ -59854,12 +60364,7 @@ var humiditySensorCapability = {
59854
60364
  } } },
59855
60365
  /** Clock fields: written, but excluded from the compare that decides
59856
60366
  * whether persisting is worth a SQLite commit. */
59857
- volatileStateFields: ["lastFetchedAt"],
59858
- customize: {
59859
- version: 1,
59860
- advanced: "derive",
59861
- targets: []
59862
- }
60367
+ volatileStateFields: ["lastFetchedAt"]
59863
60368
  };
59864
60369
  /**
59865
60370
  * Image display cap. Models a single still image exposed by an integration —
@@ -59880,7 +60385,7 @@ var ImageStatusSchema = object({
59880
60385
  * entity exposes no `entity_picture` (yet). */
59881
60386
  url: string().nullable(),
59882
60387
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59883
- lastUpdated: number().nullable()
60388
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59884
60389
  });
59885
60390
  var imageCapability = {
59886
60391
  name: "image",
@@ -59968,19 +60473,19 @@ var NormalizedRangeSchema = object({
59968
60473
  */
59969
60474
  var ImageSettingsStatusSchema = object({
59970
60475
  /** Normalized 0–100. */
59971
- brightness: number().optional(),
60476
+ brightness: number().optional().meta(readingMeta("none")),
59972
60477
  /** Normalized 0–100. */
59973
- contrast: number().optional(),
60478
+ contrast: number().optional().meta(readingMeta("none")),
59974
60479
  /** Normalized 0–100. */
59975
- saturation: number().optional(),
60480
+ saturation: number().optional().meta(readingMeta("none")),
59976
60481
  /** Normalized 0–100. */
59977
- sharpness: number().optional(),
60482
+ sharpness: number().optional().meta(readingMeta("none")),
59978
60483
  mirror: boolean().optional(),
59979
60484
  flip: boolean().optional(),
59980
60485
  rotate: ImageRotateSchema.optional(),
59981
60486
  whiteBalance: WhiteBalanceModeSchema.optional(),
59982
60487
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
59983
- warmth: number().optional(),
60488
+ warmth: number().optional().meta(readingMeta("none")),
59984
60489
  exposureMode: ExposureModeSchema.optional(),
59985
60490
  backlightMode: BacklightModeSchema.optional(),
59986
60491
  lastFetchedAt: number()
@@ -60259,10 +60764,10 @@ var IntercomAbilitySchema = object({
60259
60764
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60260
60765
  codecs: array(string()),
60261
60766
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60262
- sampleRate: number(),
60767
+ sampleRate: number().meta(readingMeta("none")),
60263
60768
  duplex: _enum(["half", "full"]),
60264
60769
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60265
- maxBacklogMs: number()
60770
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60266
60771
  });
60267
60772
  var IntercomStatusSchema = object({
60268
60773
  /** True while a talk session is open. */
@@ -60489,13 +60994,13 @@ var LawnMowerControlStatusSchema = object({
60489
60994
  activity: LawnMowerActivitySchema,
60490
60995
  /** 0..100 battery percentage. Null when the device has no battery
60491
60996
  * reading. */
60492
- batteryLevel: number().min(0).max(100).nullable(),
60997
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60493
60998
  /** 0..100 mowing-completion percentage of the current task, or null when no
60494
60999
  * task is active / progress is unavailable. */
60495
- progressPercent: number().min(0).max(100).nullable(),
61000
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60496
61001
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60497
61002
  * or null when unknown. */
60498
- currentCode: number().nullable(),
61003
+ currentCode: number().nullable().meta(readingMeta("none")),
60499
61004
  /** Human label for {@link currentCode}, or null when undecodable. */
60500
61005
  currentCodeLabel: string().nullable(),
60501
61006
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61028,7 +61533,7 @@ var MediaPlayerStatusSchema = object({
61028
61533
  state: MediaPlayerStateSchema,
61029
61534
  /** Volume as 0..100 inclusive. Null when the device has no volume
61030
61535
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61031
- volumeLevel: number().min(0).max(100).nullable(),
61536
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61032
61537
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61033
61538
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61034
61539
  isMuted: boolean().nullable(),
@@ -61041,10 +61546,10 @@ var MediaPlayerStatusSchema = object({
61041
61546
  currentMedia: MediaInfoSchema.nullable(),
61042
61547
  /** Current playback position in ms. Null when not seekable or
61043
61548
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61044
- positionMs: number().int().nonnegative().nullable(),
61549
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61045
61550
  /** Total duration of the current media in ms. Null when unknown
61046
61551
  * (live stream) or nothing is playing. */
61047
- durationMs: number().int().nonnegative().nullable(),
61552
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61048
61553
  /** Shuffle toggle. Null when no shuffle surface.
61049
61554
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61050
61555
  shuffle: boolean().nullable(),
@@ -61353,7 +61858,7 @@ var motionCustomize = {
61353
61858
  eventName: "onMotionChanged"
61354
61859
  },
61355
61860
  deviceTypes: [DeviceType.Camera],
61356
- apply: "replace",
61861
+ apply: "add",
61357
61862
  applyAuthority: {
61358
61863
  kind: "camera-sensors",
61359
61864
  timeoutField: "motionCooldownMs"
@@ -61425,7 +61930,7 @@ var MotionStatusSchema = object({
61425
61930
  * it, every provider seeded it at boot, and every session left it
61426
61931
  * `detected: true` after its detach.
61427
61932
  */
61428
- autoClearAfterMs: number().nullable()
61933
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61429
61934
  });
61430
61935
  /**
61431
61936
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61462,12 +61967,18 @@ var motionCapability = {
61462
61967
  methods: {
61463
61968
  /**
61464
61969
  * Pull the current motion state synchronously. Convenience shortcut
61465
- * for consumers that don't need the full status object; equivalent
61466
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61467
- * `getStatus` once the auto-injected status surface lands in every
61468
- * consumer.
61970
+ * for consumers that don't need the full status object. A composed
61971
+ * group answers from its slice (D944): unknown throws, it is not off.
61469
61972
  */
61470
61973
  isDetected: method(object({ deviceId: number() }), boolean()) },
61974
+ /**
61975
+ * A composed motion group serves `isDetected` from `detected` (D944).
61976
+ * Without this the cap stays uncomposable (`has-commands`).
61977
+ */
61978
+ composedMethods: { isDetected: {
61979
+ kind: "slice",
61980
+ field: "detected"
61981
+ } },
61471
61982
  events: {
61472
61983
  /**
61473
61984
  * Fires every time the runner transitions a camera between
@@ -61607,7 +62118,7 @@ var MotionZoneRegionSchema = object({
61607
62118
  * the grid region(s). */
61608
62119
  var MotionZoneStatusSchema = object({
61609
62120
  enabled: boolean(),
61610
- sensitivity: number(),
62121
+ sensitivity: number().meta(readingMeta("none")),
61611
62122
  /** Grid region(s). Today exactly one `grid` shape. */
61612
62123
  regions: array(MotionZoneRegionSchema),
61613
62124
  lastFetchedAt: number()
@@ -62114,7 +62625,7 @@ var NavigationStatusSchema = object({
62114
62625
  following: boolean(),
62115
62626
  flash: boolean(),
62116
62627
  lightMode: NavigationLightModeSchema,
62117
- lightLevel: number().min(40).max(100),
62628
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62118
62629
  /** Ms epoch when the slice was last updated. */
62119
62630
  lastChangedAt: number()
62120
62631
  });
@@ -62248,13 +62759,13 @@ var NetworkLinkStatusSchema = object({
62248
62759
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62249
62760
  * SKIP a null rather than coerce it.
62250
62761
  */
62251
- signalPercent: number().min(0).max(100).nullable(),
62762
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62252
62763
  /** Raw received signal strength in dBm, when the firmware reports one. */
62253
- rssiDbm: number().optional(),
62764
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62254
62765
  /** Network name of a wireless link, when the firmware reports it. */
62255
62766
  ssid: string().optional(),
62256
62767
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62257
- lastUpdated: number()
62768
+ lastUpdated: number().meta(readingMeta("none"))
62258
62769
  });
62259
62770
  /** The slice a provider seeds before its first read: nothing is known yet. */
62260
62771
  var NETWORK_LINK_UNKNOWN = {
@@ -62650,7 +63161,7 @@ var NotifierStatusSchema = object({
62650
63161
  lastError: string().nullable(),
62651
63162
  /** Number of deliveries currently buffered server-side (rate-limited
62652
63163
  * notifiers may queue). 0 when the provider sends synchronously. */
62653
- queueDepth: number().int().nonnegative(),
63164
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62654
63165
  /** Per-feature capability matrix — authoritative source for the
62655
63166
  * compose-form field gating. */
62656
63167
  supports: NotifierSupportsSchema
@@ -62742,11 +63253,9 @@ onSent: { data: object({
62742
63253
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62743
63254
  * for: water flow, distance, weight, frequency, signal strength, …).
62744
63255
  *
62745
- * The `unit` and `precision` fields carry the live display metadata that
62746
- * the upstream source provides with each state push (HA
62747
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62748
- * The UI reads them directly from the slice — they are the single source
62749
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63256
+ * The quantity is not on this slice. It is chosen when the device is
63257
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63258
+ * that quantity's base unit. `null` is a refused sample (D393).
62750
63259
  *
62751
63260
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62752
63261
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62755,15 +63264,8 @@ onSent: { data: object({
62755
63264
  * numeric values land here.
62756
63265
  */
62757
63266
  var NumericSensorStatusSchema = object({
62758
- value: number(),
62759
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62760
- * Populated live from the upstream source on each state push.
62761
- * Absent for unitless sensors. */
62762
- unit: string().optional(),
62763
- /** Suggested decimal places for numeric display.
62764
- * Populated live from the upstream source when provided.
62765
- * Falls back to auto-formatting when absent. */
62766
- precision: number().int().min(0).max(10).optional(),
63267
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63268
+ value: number().nullable().meta(readingMeta("device")),
62767
63269
  /** Ms epoch when the slice was last updated. */
62768
63270
  lastFetchedAt: number()
62769
63271
  });
@@ -62787,11 +63289,11 @@ var numericSensorCapability = {
62787
63289
  */
62788
63290
  durability: "restored",
62789
63291
  /**
62790
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63292
+ * History (D850): recorded series. The unit is the device quantity's base,
63293
+ * resolved when the series is read — the cap has no single unit.
62791
63294
  */
62792
63295
  history: { fields: { value: {
62793
63296
  kind: "gauge",
62794
- unitFrom: "unit",
62795
63297
  minIntervalMs: 3e4
62796
63298
  } } },
62797
63299
  /** Clock fields: written, but excluded from the compare that decides
@@ -63083,23 +63585,26 @@ var PetFeederStatusSchema = object({
63083
63585
  /** Food currently in the bowl (grams). Null when the device has not
63084
63586
  * reported a reading yet. On dual-hopper models this is the combined
63085
63587
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63086
- foodLevel: number().nullable(),
63588
+ foodLevel: number().nullable().meta(readingMeta("none")),
63087
63589
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63088
63590
  * single-hopper models. */
63089
- food1: number().nullable(),
63591
+ food1: number().nullable().meta(readingMeta("none")),
63090
63592
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63091
63593
  * single-hopper models. */
63092
- food2: number().nullable(),
63594
+ food2: number().nullable().meta(readingMeta("none")),
63093
63595
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63094
63596
  * (`device_class: problem`, on = low). True when the bowl is empty /
63095
63597
  * below the feeder's low threshold. */
63096
63598
  lowFood: boolean(),
63097
63599
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63098
63600
  * device has no battery reading. */
63099
- batteryPower: number().min(0).max(100).nullable(),
63100
- /** Days of desiccant life remaining. Null when the model has no
63101
- * desiccant sensor. */
63102
- desiccantLeftDays: number().nullable(),
63601
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63602
+ /**
63603
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63604
+ * Providers convert from the days the feeder reports. Null when the
63605
+ * model has no desiccant sensor.
63606
+ */
63607
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63103
63608
  /** True while a feed is in progress. */
63104
63609
  feeding: boolean(),
63105
63610
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63110,7 +63615,7 @@ var PetFeederStatusSchema = object({
63110
63615
  * with `errorCode` for consumers that want the raw integer. */
63111
63616
  error: string().nullable(),
63112
63617
  /** Raw device error code (0 / null = no error). */
63113
- errorCode: number().nullable(),
63618
+ errorCode: number().nullable().meta(readingMeta("none")),
63114
63619
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63115
63620
  isDualHopper: boolean(),
63116
63621
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63120,7 +63625,7 @@ var PetFeederStatusSchema = object({
63120
63625
  /** Play a chime when dispensing. */
63121
63626
  feedSound: boolean(),
63122
63627
  /** Speaker / prompt volume level (device-scaled integer). */
63123
- volume: number(),
63628
+ volume: number().meta(readingMeta("none")),
63124
63629
  /** Ms epoch when the slice was last refreshed from the cloud. */
63125
63630
  lastFetchedAt: number()
63126
63631
  });
@@ -63239,7 +63744,7 @@ var petFeederCapability = {
63239
63744
  },
63240
63745
  desiccantLeftDays: {
63241
63746
  kind: "gauge",
63242
- unit: "days"
63747
+ unit: "s"
63243
63748
  }
63244
63749
  } }
63245
63750
  };
@@ -63640,36 +64145,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63640
64145
  */
63641
64146
  var PowerMeterStatusSchema = object({
63642
64147
  /** Instantaneous power draw in watts. */
63643
- watts: number().optional().meta({
63644
- unit: "W",
63645
- label: "Power"
63646
- }),
64148
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63647
64149
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63648
- kwhTotal: number().optional().meta({
63649
- unit: "kWh",
63650
- label: "Energy"
63651
- }),
64150
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63652
64151
  /** Voltage in volts. */
63653
- volts: number().optional(),
64152
+ volts: number().optional().meta(readingMeta("voltage")),
63654
64153
  /** Current in amperes. */
63655
- amps: number().optional(),
63656
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63657
- va: number().optional(),
64154
+ amps: number().optional().meta(readingMeta("current")),
64155
+ /** Apparent power in volt-amperes. */
64156
+ va: number().optional().meta(readingMeta("apparent-power")),
63658
64157
  /** Power factor (0..1) if reported. */
63659
- powerFactor: number().min(0).max(1).optional(),
64158
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63660
64159
  /** Ms epoch when the slice was last updated. */
63661
- lastFetchedAt: number(),
63662
- /** Live display unit of the single metric this slice carries (e.g. HA
63663
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63664
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63665
- * unit per slice is unambiguous. The UI prefers this over the role's
63666
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63667
- unit: string().optional(),
63668
- /** Suggested decimal places for numeric display.
63669
- * Populated live from the upstream source when provided (e.g. HA
63670
- * `attributes.suggested_display_precision`). Falls back to
63671
- * auto-formatting when absent. */
63672
- precision: number().int().min(0).max(10).optional()
64160
+ lastFetchedAt: number()
63673
64161
  });
63674
64162
  var powerMeterCapability = {
63675
64163
  name: "power-meter",
@@ -63733,12 +64221,7 @@ var powerMeterCapability = {
63733
64221
  } },
63734
64222
  /** Clock fields: written, but excluded from the compare that decides
63735
64223
  * whether persisting is worth a SQLite commit. */
63736
- volatileStateFields: ["lastFetchedAt"],
63737
- customize: {
63738
- version: 1,
63739
- advanced: "derive",
63740
- targets: []
63741
- }
64224
+ volatileStateFields: ["lastFetchedAt"]
63742
64225
  };
63743
64226
  /**
63744
64227
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63758,11 +64241,11 @@ var powerMeterCapability = {
63758
64241
  */
63759
64242
  var GpsLocationSchema = object({
63760
64243
  /** Latitude in decimal degrees, -90..90. */
63761
- latitude: number().min(-90).max(90),
64244
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63762
64245
  /** Longitude in decimal degrees, -180..180. */
63763
- longitude: number().min(-180).max(180),
64246
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63764
64247
  /** Reported accuracy in meters (lower = better). */
63765
- accuracyMeters: number().nonnegative()
64248
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63766
64249
  });
63767
64250
  var PresenceStatusSchema = object({
63768
64251
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63774,7 +64257,7 @@ var PresenceStatusSchema = object({
63774
64257
  gps: GpsLocationSchema.nullable(),
63775
64258
  /** Optional battery level of the tracking device (0..100). Null
63776
64259
  * when not reported. */
63777
- batteryPercent: number().min(0).max(100).nullable(),
64260
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63778
64261
  /** Ms epoch when the slice was last updated. */
63779
64262
  lastChangedAt: number()
63780
64263
  });
@@ -63824,19 +64307,10 @@ var presenceCapability = {
63824
64307
  * `sourceInfo`.
63825
64308
  */
63826
64309
  var PressureSensorStatusSchema = object({
63827
- /** Current pressure in hPa. */
63828
- hpa: number(),
64310
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64311
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63829
64312
  /** Ms epoch when the slice was last updated. */
63830
- lastFetchedAt: number(),
63831
- /** Live display unit from the upstream source (e.g. HA
63832
- * `attributes.unit_of_measurement`). The UI prefers this over the
63833
- * role's canonical unit. Absent → fall back to the canonical unit. */
63834
- unit: string().optional(),
63835
- /** Suggested decimal places for numeric display.
63836
- * Populated live from the upstream source when provided (e.g. HA
63837
- * `attributes.suggested_display_precision`). Falls back to
63838
- * auto-formatting when absent. */
63839
- precision: number().int().min(0).max(10).optional()
64313
+ lastFetchedAt: number()
63840
64314
  });
63841
64315
  var pressureSensorCapability = {
63842
64316
  name: "pressure-sensor",
@@ -64250,8 +64724,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64250
64724
  }), _void(), { kind: "mutation" });
64251
64725
  var PtzAutotrackSettingsSchema = object({
64252
64726
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64253
- stopDelaySeconds: number().int().min(0).max(300),
64254
- disappearDelaySeconds: number().int().min(0).max(300)
64727
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64728
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64255
64729
  });
64256
64730
  /**
64257
64731
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64413,7 +64887,7 @@ var RunningStateStatusSchema = object({
64413
64887
  /** null = unknown — never coerced to false (D393). */
64414
64888
  running: boolean().nullable(),
64415
64889
  /** Epoch ms the current boolean began; null while `running` is null. */
64416
- since: number().nullable(),
64890
+ since: number().nullable().meta(readingMeta("none")),
64417
64891
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64418
64892
  phase: string().nullable(),
64419
64893
  /** null until a reading has been published (D393). */
@@ -64428,7 +64902,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64428
64902
  /** Generic Customize target for the running boolean (D936). */
64429
64903
  var runningCustomize = {
64430
64904
  version: 1,
64431
- advanced: "derive",
64432
64905
  targets: [{
64433
64906
  id: "running",
64434
64907
  cardinality: "scalar",
@@ -64448,7 +64921,7 @@ var runningCustomize = {
64448
64921
  DeviceType.Thermostat,
64449
64922
  DeviceType.PetFeeder
64450
64923
  ],
64451
- apply: "add-or-replace",
64924
+ apply: "add",
64452
64925
  path: "running",
64453
64926
  sources: [
64454
64927
  {
@@ -65712,9 +66185,9 @@ var RecordTriggerSchema = _enum([
65712
66185
  */
65713
66186
  var RecordWindowSchema = object({
65714
66187
  trigger: RecordTriggerSchema,
65715
- day: number().int().min(0).max(6),
65716
- startMinute: number().int().min(0).max(1439),
65717
- endMinute: number().int().min(1).max(1440)
66188
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66189
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66190
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65718
66191
  });
65719
66192
  /** Status of one physical volume, as the camera itself describes it. */
65720
66193
  var OnboardStorageVolumeSchema = object({
@@ -65790,10 +66263,10 @@ var RecordingOnboardStatusSchema = object({
65790
66263
  /** Master "record to the card at all" switch. */
65791
66264
  enabled: boolean().nullable(),
65792
66265
  overwriteWhenFull: boolean().nullable(),
65793
- preRecordSec: number().nullable(),
65794
- postRecordSec: number().nullable(),
66266
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66267
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65795
66268
  /** Length of one recorded file, in minutes. */
65796
- segmentMinutes: number().nullable(),
66269
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65797
66270
  /** The primary track's weekly windows, flattened. */
65798
66271
  windows: array(RecordWindowSchema),
65799
66272
  /**
@@ -65809,7 +66282,7 @@ var RecordingOnboardStatusSchema = object({
65809
66282
  * provider reporting a non-zero count also reports the schedule as not
65810
66283
  * writable.
65811
66284
  */
65812
- unreadableWindows: number(),
66285
+ unreadableWindows: number().meta(readingMeta("count")),
65813
66286
  /**
65814
66287
  * The camera is scheduled to record and has NO usable storage.
65815
66288
  *
@@ -66654,7 +67127,7 @@ var CamStreamDescriptorSchema = object({
66654
67127
  url: string().optional(),
66655
67128
  codec: string().optional(),
66656
67129
  resolution: CamStreamResolutionSchema.optional(),
66657
- fps: number().positive().optional(),
67130
+ fps: number().positive().optional().meta(readingMeta("rate")),
66658
67131
  label: string().optional(),
66659
67132
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66660
67133
  deviceFeatures: array(string()).optional(),
@@ -66741,18 +67214,18 @@ var StreamProfileSchema = _enum([
66741
67214
  ]);
66742
67215
  /** The current encoding config of one stream profile. */
66743
67216
  var StreamProfileConfigSchema = object({
66744
- width: number(),
66745
- height: number(),
67217
+ width: number().meta(readingMeta("none")),
67218
+ height: number().meta(readingMeta("none")),
66746
67219
  codec: _enum(["h264", "h265"]),
66747
- framerate: number(),
66748
- bitrate: number(),
67220
+ framerate: number().meta(readingMeta("rate")),
67221
+ bitrate: number().meta(readingMeta("bitrate")),
66749
67222
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66750
67223
  encoderProfile: _enum([
66751
67224
  "high",
66752
67225
  "main",
66753
67226
  "baseline"
66754
67227
  ]).optional(),
66755
- gop: number().optional(),
67228
+ gop: number().optional().meta(readingMeta("none")),
66756
67229
  /**
66757
67230
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66758
67231
  *
@@ -66973,12 +67446,7 @@ var switchCapability = {
66973
67446
  label: "On",
66974
67447
  kind: "boolean"
66975
67448
  }
66976
- }] },
66977
- customize: {
66978
- version: 1,
66979
- advanced: "derive",
66980
- targets: []
66981
- }
67449
+ }] }
66982
67450
  };
66983
67451
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66984
67452
  function hasControlCharacter(value) {
@@ -67561,35 +68029,21 @@ var tamperCapability = {
67561
68029
  * entries with `device_class: temperature` and any future native
67562
68030
  * thermometer.
67563
68031
  *
67564
- * Unit normalisation: providers convert to Celsius before storing.
67565
- * The slice value is always Celsius so cross-cap aggregators
67566
- * (climate-control's `currentTemp`, energy analytics) can compose
67567
- * without per-source unit fixups. The canonical display unit (`°C`) is
67568
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67569
- * `sourceInfo`.
68032
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68033
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67570
68034
  *
67571
68035
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67572
68036
  * frozen feed (provider hung) distinct from a "temperature hasn't
67573
68037
  * changed" steady state.
67574
68038
  */
67575
68039
  var TemperatureSensorStatusSchema = object({
67576
- /** Current temperature in Celsius. */
67577
- celsius: number().meta({
67578
- unit: "°C",
68040
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68041
+ celsius: number().nullable().meta(readingMeta("temperature", {
68042
+ precision: 1,
67579
68043
  label: "Temperature"
67580
- }),
68044
+ })),
67581
68045
  /** Ms epoch when the slice was last updated (push or poll). */
67582
- lastFetchedAt: number(),
67583
- /** Live display unit from the upstream source (e.g. HA
67584
- * `attributes.unit_of_measurement`). The UI prefers this over the
67585
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67586
- * Absent → the UI falls back to the role's canonical unit. */
67587
- unit: string().optional(),
67588
- /** Suggested decimal places for numeric display.
67589
- * Populated live from the upstream source when provided (e.g. HA
67590
- * `attributes.suggested_display_precision`). Falls back to
67591
- * auto-formatting when absent. */
67592
- precision: number().int().min(0).max(10).optional()
68046
+ lastFetchedAt: number()
67593
68047
  });
67594
68048
  var temperatureSensorCapability = {
67595
68049
  name: "temperature-sensor",
@@ -67620,12 +68074,7 @@ var temperatureSensorCapability = {
67620
68074
  } } },
67621
68075
  /** Clock fields: written, but excluded from the compare that decides
67622
68076
  * whether persisting is worth a SQLite commit. */
67623
- volatileStateFields: ["lastFetchedAt"],
67624
- customize: {
67625
- version: 1,
67626
- advanced: "derive",
67627
- targets: []
67628
- }
68077
+ volatileStateFields: ["lastFetchedAt"]
67629
68078
  };
67630
68079
  /**
67631
68080
  * toast — system-scoped singleton capability that streams toast
@@ -67908,16 +68357,16 @@ var VacuumControlStatusSchema = object({
67908
68357
  ]),
67909
68358
  /** 0..100 battery percentage. Null when the device has no battery
67910
68359
  * reading. */
67911
- batteryLevel: number().min(0).max(100).nullable(),
68360
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67912
68361
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67913
68362
  * the vacuum has no speed control. */
67914
68363
  fanSpeed: string().nullable(),
67915
68364
  /** Speed tokens the hardware accepts — drives the UI selector. */
67916
68365
  availableFanSpeeds: array(string()),
67917
68366
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67918
- progressPercent: number().min(0).max(100).nullable(),
68367
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67919
68368
  /** Current error code (0 / null = no error). */
67920
- errorCode: number().nullable(),
68369
+ errorCode: number().nullable().meta(readingMeta("none")),
67921
68370
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67922
68371
  errorLabel: string().nullable(),
67923
68372
  /** Ms epoch when the slice was last updated. */
@@ -67995,7 +68444,7 @@ var ValveStatusSchema = object({
67995
68444
  ]),
67996
68445
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67997
68446
  * intermediate position surface. */
67998
- position: number().min(0).max(100).nullable(),
68447
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67999
68448
  /** Ms epoch when the slice was last updated. */
68000
68449
  lastChangedAt: number()
68001
68450
  });
@@ -68103,9 +68552,9 @@ var vibrationCapability = {
68103
68552
  */
68104
68553
  var WaterHeaterStatusSchema = object({
68105
68554
  /** Current measured temperature. Null when not reported. */
68106
- currentTemp: number().nullable(),
68555
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68107
68556
  /** Target temperature setpoint. Null when no setpoint surface. */
68108
- targetTemp: number().nullable(),
68557
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68109
68558
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68110
68559
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68111
68560
  * device reports an unknown state. */
@@ -68116,9 +68565,9 @@ var WaterHeaterStatusSchema = object({
68116
68565
  * has no away surface. */
68117
68566
  away: boolean().nullable(),
68118
68567
  /** HA `min_temp` attribute. Null when not reported. */
68119
- minTemp: number().nullable(),
68568
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68120
68569
  /** HA `max_temp` attribute. Null when not reported. */
68121
- maxTemp: number().nullable(),
68570
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68122
68571
  /** Ms epoch when the slice was last updated. */
68123
68572
  lastChangedAt: number()
68124
68573
  });
@@ -68203,25 +68652,18 @@ var waterHeaterCapability = {
68203
68652
  * Forecast deferred — current conditions only for v1.
68204
68653
  */
68205
68654
  var WeatherStatusSchema = object({
68206
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68207
- * when no condition has been reported yet. */
68655
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68208
68656
  condition: string().nullable(),
68209
- /** Current temperature in the reported unit. Null when not provided. */
68210
- temperature: number().nullable(),
68211
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68212
- temperatureUnit: string().nullable(),
68657
+ /** Current temperature in Celsius. Null when not provided or refused. */
68658
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68213
68659
  /** Relative humidity (0..100). Null when not provided. */
68214
- humidity: number().min(0).max(100).nullable(),
68215
- /** Barometric pressure in the reported unit. Null when not provided. */
68216
- pressure: number().nullable(),
68217
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68218
- pressureUnit: string().nullable(),
68219
- /** Wind speed in the reported unit. Null when not provided. */
68220
- windSpeed: number().nullable(),
68221
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68222
- windSpeedUnit: string().nullable(),
68660
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68661
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68662
+ pressure: number().nullable().meta(readingMeta("pressure")),
68663
+ /** Wind speed in metres per second. Null when not provided or refused. */
68664
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68223
68665
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68224
- windBearing: number().nullable(),
68666
+ windBearing: number().nullable().meta(readingMeta("angle")),
68225
68667
  /** Ms epoch when the slice was last updated. */
68226
68668
  lastFetchedAt: number()
68227
68669
  });
@@ -68249,12 +68691,12 @@ var weatherCapability = {
68249
68691
  */
68250
68692
  durability: "session",
68251
68693
  /**
68252
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68694
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68253
68695
  */
68254
68696
  history: { fields: {
68255
68697
  temperature: {
68256
68698
  kind: "gauge",
68257
- unitFrom: "temperatureUnit",
68699
+ unit: "°C",
68258
68700
  deadband: .2
68259
68701
  },
68260
68702
  humidity: {
@@ -68264,12 +68706,12 @@ var weatherCapability = {
68264
68706
  },
68265
68707
  pressure: {
68266
68708
  kind: "gauge",
68267
- unitFrom: "pressureUnit",
68709
+ unit: "hPa",
68268
68710
  deadband: .5
68269
68711
  },
68270
68712
  windSpeed: {
68271
68713
  kind: "gauge",
68272
- unitFrom: "windSpeedUnit",
68714
+ unit: "m/s",
68273
68715
  deadband: .5
68274
68716
  }
68275
68717
  } }
@@ -69200,6 +69642,404 @@ function runningFromLawnMower(activity, previous, fetchedAt, transitionAt) {
69200
69642
  if (activity === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
69201
69643
  return nativeSlice(false, activity, previous, transitionAtMs, fetchedAt, null);
69202
69644
  }
69645
+ /**
69646
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
69647
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
69648
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
69649
+ * decimals). K derives from `°C = K − 273.15`.
69650
+ */
69651
+ var UNIT_TABLE = {
69652
+ "°C": {
69653
+ dimension: "temperature",
69654
+ factor: 1,
69655
+ offset: 0
69656
+ },
69657
+ "°F": {
69658
+ dimension: "temperature",
69659
+ factor: 5 / 9,
69660
+ offset: -160 / 9
69661
+ },
69662
+ K: {
69663
+ dimension: "temperature",
69664
+ factor: 1,
69665
+ offset: -273.15
69666
+ },
69667
+ hPa: {
69668
+ dimension: "pressure",
69669
+ factor: 1,
69670
+ offset: 0
69671
+ },
69672
+ kPa: {
69673
+ dimension: "pressure",
69674
+ factor: 10,
69675
+ offset: 0
69676
+ },
69677
+ Pa: {
69678
+ dimension: "pressure",
69679
+ factor: .01,
69680
+ offset: 0
69681
+ },
69682
+ mbar: {
69683
+ dimension: "pressure",
69684
+ factor: 1,
69685
+ offset: 0
69686
+ },
69687
+ bar: {
69688
+ dimension: "pressure",
69689
+ factor: 1e3,
69690
+ offset: 0
69691
+ },
69692
+ inHg: {
69693
+ dimension: "pressure",
69694
+ factor: 33.8639,
69695
+ offset: 0
69696
+ },
69697
+ mmHg: {
69698
+ dimension: "pressure",
69699
+ factor: 1.33322,
69700
+ offset: 0
69701
+ },
69702
+ psi: {
69703
+ dimension: "pressure",
69704
+ factor: 68.9476,
69705
+ offset: 0
69706
+ },
69707
+ "m/s": {
69708
+ dimension: "speed",
69709
+ factor: 1,
69710
+ offset: 0
69711
+ },
69712
+ "km/h": {
69713
+ dimension: "speed",
69714
+ factor: 1 / 3.6,
69715
+ offset: 0
69716
+ },
69717
+ mph: {
69718
+ dimension: "speed",
69719
+ factor: .44704,
69720
+ offset: 0
69721
+ },
69722
+ kn: {
69723
+ dimension: "speed",
69724
+ factor: .514444,
69725
+ offset: 0
69726
+ },
69727
+ "mm/h": {
69728
+ dimension: "precipitation-rate",
69729
+ factor: 1,
69730
+ offset: 0
69731
+ },
69732
+ "in/h": {
69733
+ dimension: "precipitation-rate",
69734
+ factor: 25.4,
69735
+ offset: 0
69736
+ },
69737
+ m: {
69738
+ dimension: "length",
69739
+ factor: 1,
69740
+ offset: 0
69741
+ },
69742
+ mm: {
69743
+ dimension: "length",
69744
+ factor: .001,
69745
+ offset: 0
69746
+ },
69747
+ cm: {
69748
+ dimension: "length",
69749
+ factor: .01,
69750
+ offset: 0
69751
+ },
69752
+ km: {
69753
+ dimension: "length",
69754
+ factor: 1e3,
69755
+ offset: 0
69756
+ },
69757
+ in: {
69758
+ dimension: "length",
69759
+ factor: .0254,
69760
+ offset: 0
69761
+ },
69762
+ ft: {
69763
+ dimension: "length",
69764
+ factor: .3048,
69765
+ offset: 0
69766
+ },
69767
+ mi: {
69768
+ dimension: "length",
69769
+ factor: 1609.344,
69770
+ offset: 0
69771
+ },
69772
+ "m²": {
69773
+ dimension: "area",
69774
+ factor: 1,
69775
+ offset: 0
69776
+ },
69777
+ lx: {
69778
+ dimension: "illuminance",
69779
+ factor: 1,
69780
+ offset: 0
69781
+ },
69782
+ "W/m²": {
69783
+ dimension: "irradiance",
69784
+ factor: 1,
69785
+ offset: 0
69786
+ },
69787
+ W: {
69788
+ dimension: "power",
69789
+ factor: 1,
69790
+ offset: 0
69791
+ },
69792
+ kW: {
69793
+ dimension: "power",
69794
+ factor: 1e3,
69795
+ offset: 0
69796
+ },
69797
+ VA: {
69798
+ dimension: "apparent-power",
69799
+ factor: 1,
69800
+ offset: 0
69801
+ },
69802
+ Wh: {
69803
+ dimension: "energy",
69804
+ factor: 1,
69805
+ offset: 0
69806
+ },
69807
+ kWh: {
69808
+ dimension: "energy",
69809
+ factor: 1e3,
69810
+ offset: 0
69811
+ },
69812
+ MWh: {
69813
+ dimension: "energy",
69814
+ factor: 1e6,
69815
+ offset: 0
69816
+ },
69817
+ V: {
69818
+ dimension: "voltage",
69819
+ factor: 1,
69820
+ offset: 0
69821
+ },
69822
+ mV: {
69823
+ dimension: "voltage",
69824
+ factor: .001,
69825
+ offset: 0
69826
+ },
69827
+ A: {
69828
+ dimension: "current",
69829
+ factor: 1,
69830
+ offset: 0
69831
+ },
69832
+ mA: {
69833
+ dimension: "current",
69834
+ factor: .001,
69835
+ offset: 0
69836
+ },
69837
+ ppm: {
69838
+ dimension: "concentration-volume",
69839
+ factor: 1,
69840
+ offset: 0
69841
+ },
69842
+ ppb: {
69843
+ dimension: "concentration-volume",
69844
+ factor: .001,
69845
+ offset: 0
69846
+ },
69847
+ "µg/m³": {
69848
+ dimension: "concentration-mass",
69849
+ factor: 1,
69850
+ offset: 0
69851
+ },
69852
+ "mg/m³": {
69853
+ dimension: "concentration-mass",
69854
+ factor: 1e3,
69855
+ offset: 0
69856
+ },
69857
+ kg: {
69858
+ dimension: "mass",
69859
+ factor: 1,
69860
+ offset: 0
69861
+ },
69862
+ g: {
69863
+ dimension: "mass",
69864
+ factor: .001,
69865
+ offset: 0
69866
+ },
69867
+ lb: {
69868
+ dimension: "mass",
69869
+ factor: .453592,
69870
+ offset: 0
69871
+ },
69872
+ oz: {
69873
+ dimension: "mass",
69874
+ factor: .0283495,
69875
+ offset: 0
69876
+ },
69877
+ "%": {
69878
+ dimension: "percentage",
69879
+ factor: 1,
69880
+ offset: 0
69881
+ },
69882
+ L: {
69883
+ dimension: "volume",
69884
+ factor: 1,
69885
+ offset: 0
69886
+ },
69887
+ mL: {
69888
+ dimension: "volume",
69889
+ factor: .001,
69890
+ offset: 0
69891
+ },
69892
+ gal: {
69893
+ dimension: "volume",
69894
+ factor: 3.785411784,
69895
+ offset: 0
69896
+ },
69897
+ s: {
69898
+ dimension: "duration",
69899
+ factor: 1,
69900
+ offset: 0
69901
+ },
69902
+ ms: {
69903
+ dimension: "duration",
69904
+ factor: .001,
69905
+ offset: 0
69906
+ },
69907
+ min: {
69908
+ dimension: "duration",
69909
+ factor: 60,
69910
+ offset: 0
69911
+ },
69912
+ h: {
69913
+ dimension: "duration",
69914
+ factor: 3600,
69915
+ offset: 0
69916
+ },
69917
+ d: {
69918
+ dimension: "duration",
69919
+ factor: 86400,
69920
+ offset: 0
69921
+ },
69922
+ B: {
69923
+ dimension: "information",
69924
+ factor: 1,
69925
+ offset: 0
69926
+ },
69927
+ KiB: {
69928
+ dimension: "information",
69929
+ factor: 1024,
69930
+ offset: 0
69931
+ },
69932
+ MiB: {
69933
+ dimension: "information",
69934
+ factor: 1024 ** 2,
69935
+ offset: 0
69936
+ },
69937
+ GiB: {
69938
+ dimension: "information",
69939
+ factor: 1024 ** 3,
69940
+ offset: 0
69941
+ },
69942
+ TiB: {
69943
+ dimension: "information",
69944
+ factor: 1024 ** 4,
69945
+ offset: 0
69946
+ },
69947
+ kB: {
69948
+ dimension: "information",
69949
+ factor: 1e3,
69950
+ offset: 0
69951
+ },
69952
+ MB: {
69953
+ dimension: "information",
69954
+ factor: 1e6,
69955
+ offset: 0
69956
+ },
69957
+ GB: {
69958
+ dimension: "information",
69959
+ factor: 1e9,
69960
+ offset: 0
69961
+ },
69962
+ "°": {
69963
+ dimension: "angle",
69964
+ factor: 1,
69965
+ offset: 0
69966
+ },
69967
+ dBFS: {
69968
+ dimension: "sound-level",
69969
+ factor: 1,
69970
+ offset: 0
69971
+ },
69972
+ dBm: {
69973
+ dimension: "signal-level",
69974
+ factor: 1,
69975
+ offset: 0
69976
+ },
69977
+ mired: {
69978
+ dimension: "colour-temperature",
69979
+ factor: 1,
69980
+ offset: 0
69981
+ },
69982
+ kbps: {
69983
+ dimension: "bitrate",
69984
+ factor: 1,
69985
+ offset: 0
69986
+ },
69987
+ Mbps: {
69988
+ dimension: "bitrate",
69989
+ factor: 1e3,
69990
+ offset: 0
69991
+ },
69992
+ fps: {
69993
+ dimension: "rate",
69994
+ factor: 1,
69995
+ offset: 0
69996
+ }
69997
+ };
69998
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
69999
+ function specFor(unit) {
70000
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
70001
+ }
70002
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
70003
+ function unitDimension(unit) {
70004
+ return specFor(unit)?.dimension;
70005
+ }
70006
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
70007
+ function toCanonical(value, spec) {
70008
+ return value * spec.factor + spec.offset;
70009
+ }
70010
+ /** Canonical-unit value → the unit described by `spec`. */
70011
+ function fromCanonical(canonical, spec) {
70012
+ return (canonical - spec.offset) / spec.factor;
70013
+ }
70014
+ /**
70015
+ * Graceful variant for the render path: returns the converted value, or
70016
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
70017
+ * Never throws — a render caller falls back to the raw value + source unit.
70018
+ */
70019
+ function tryConvertUnit(value, from, to) {
70020
+ if (from === to) return value;
70021
+ const fromSpec = specFor(from);
70022
+ if (!fromSpec) return void 0;
70023
+ const toSpec = specFor(to);
70024
+ if (!toSpec) return void 0;
70025
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
70026
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
70027
+ }
70028
+ /**
70029
+ * Every known unit spelling in a dimension (table insertion order — canonical
70030
+ * unit first). Powers the UI's same-dimension unit picker.
70031
+ */
70032
+ /**
70033
+ * Convert a difference, not a reading. Affine units (°F) shift an absolute
70034
+ * value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
70035
+ */
70036
+ function tryConvertDelta(value, from, to) {
70037
+ if (from === to) return value;
70038
+ const origin = tryConvertUnit(0, from, to);
70039
+ const shifted = tryConvertUnit(value, from, to);
70040
+ if (origin === void 0 || shifted === void 0) return void 0;
70041
+ return shifted - origin;
70042
+ }
69203
70043
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69204
70044
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Camera, DeviceType.Camera, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
69205
70045
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -69530,6 +70370,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69530
70370
  featureProbe: featureProbeCapability,
69531
70371
  flood: floodCapability,
69532
70372
  gas: gasCapability,
70373
+ groupCommands: groupCommandsCapability,
70374
+ groupStatus: groupStatusCapability,
69533
70375
  heatingValve: heatingValveCapability,
69534
70376
  humidifier: humidifierCapability,
69535
70377
  humiditySensor: humiditySensorCapability,
@@ -70922,6 +71764,13 @@ Object.freeze({
70922
71764
  access: "view",
70923
71765
  role: "admin"
70924
71766
  },
71767
+ "addons.getPackageDocs": {
71768
+ capName: "addons",
71769
+ capScope: "system",
71770
+ addonId: null,
71771
+ access: "view",
71772
+ role: "admin"
71773
+ },
70925
71774
  "addons.getVersions": {
70926
71775
  capName: "addons",
70927
71776
  capScope: "system",
@@ -74030,6 +74879,20 @@ Object.freeze({
74030
74879
  access: "view",
74031
74880
  role: "viewer"
74032
74881
  },
74882
+ "groupCommands.getStatus": {
74883
+ capName: "group-commands",
74884
+ capScope: "device",
74885
+ addonId: null,
74886
+ access: "view",
74887
+ role: "viewer"
74888
+ },
74889
+ "groupStatus.getStatus": {
74890
+ capName: "group-status",
74891
+ capScope: "device",
74892
+ addonId: null,
74893
+ access: "view",
74894
+ role: "viewer"
74895
+ },
74033
74896
  "heatingValve.getStatus": {
74034
74897
  capName: "heating-valve",
74035
74898
  capScope: "device",
@@ -83852,6 +84715,37 @@ var SENSOR_CAP_VOCABULARY = {
83852
84715
  icon: "flame"
83853
84716
  }]
83854
84717
  },
84718
+ "group-commands": {
84719
+ scope: "device",
84720
+ noun: t("Group command", "Comando di gruppo")
84721
+ },
84722
+ "group-status": {
84723
+ scope: "device",
84724
+ noun: t("Group", "Gruppo"),
84725
+ state: [
84726
+ {
84727
+ kind: "numeric",
84728
+ field: "included",
84729
+ quantity: t("the members reporting", "i membri che rispondono"),
84730
+ label: t("Reporting", "Rispondono"),
84731
+ icon: "users"
84732
+ },
84733
+ {
84734
+ kind: "numeric",
84735
+ field: "excluded",
84736
+ quantity: t("the members left out", "i membri esclusi"),
84737
+ label: t("Excluded", "Esclusi"),
84738
+ icon: "users"
84739
+ },
84740
+ {
84741
+ kind: "numeric",
84742
+ field: "total",
84743
+ quantity: t("the member count", "il numero di membri"),
84744
+ label: t("Members", "Membri"),
84745
+ icon: "sigma"
84746
+ }
84747
+ ]
84748
+ },
83855
84749
  "humidity-sensor": {
83856
84750
  scope: "device",
83857
84751
  noun: t("Humidity", "Umidità"),
@@ -83885,7 +84779,6 @@ var SENSOR_CAP_VOCABULARY = {
83885
84779
  field: "value",
83886
84780
  quantity: t("the reading", "il valore"),
83887
84781
  label: t("Reading", "Valore"),
83888
- unitField: "unit",
83889
84782
  icon: "gauge"
83890
84783
  }]
83891
84784
  },
@@ -84021,7 +84914,7 @@ var SENSOR_CAP_VOCABULARY = {
84021
84914
  field: "temperature",
84022
84915
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84023
84916
  label: t("Temperature", "Temperatura"),
84024
- unitField: "temperatureUnit",
84917
+ unit: "°C",
84025
84918
  icon: "thermometer"
84026
84919
  },
84027
84920
  {
@@ -84037,7 +84930,7 @@ var SENSOR_CAP_VOCABULARY = {
84037
84930
  field: "windSpeed",
84038
84931
  quantity: t("the wind speed", "la velocità del vento"),
84039
84932
  label: t("Wind", "Vento"),
84040
- unitField: "windSpeedUnit",
84933
+ unit: "m/s",
84041
84934
  icon: "wind"
84042
84935
  },
84043
84936
  {
@@ -84045,7 +84938,7 @@ var SENSOR_CAP_VOCABULARY = {
84045
84938
  field: "pressure",
84046
84939
  quantity: t("the air pressure", "la pressione atmosferica"),
84047
84940
  label: t("Pressure", "Pressione"),
84048
- unitField: "pressureUnit",
84941
+ unit: "hPa",
84049
84942
  icon: "gauge"
84050
84943
  }
84051
84944
  ]
@@ -85318,6 +86211,16 @@ Object.freeze({
85318
86211
  form: "single",
85319
86212
  optional: false
85320
86213
  }],
86214
+ "groupCommands.getStatus": [{
86215
+ name: "deviceId",
86216
+ form: "single",
86217
+ optional: false
86218
+ }],
86219
+ "groupStatus.getStatus": [{
86220
+ name: "deviceId",
86221
+ form: "single",
86222
+ optional: false
86223
+ }],
85321
86224
  "heatingValve.getStatus": [{
85322
86225
  name: "deviceId",
85323
86226
  form: "single",
@@ -87302,6 +88205,8 @@ new Map(Object.entries({
87302
88205
  "detection-blind": "device-events",
87303
88206
  "motion-noisy": "device-events",
87304
88207
  "motion-calm": "device-events",
88208
+ "package-llm-silent": "device-events",
88209
+ "package-llm-answered": "device-events",
87305
88210
  "recording-stalled": "device-events",
87306
88211
  "recording-healthy": "device-events",
87307
88212
  "recording-degraded": "device-events",
@@ -87385,6 +88290,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87385
88290
  label: "Motion without a subject",
87386
88291
  kinds: ["motion-noisy", "motion-calm"]
87387
88292
  },
88293
+ {
88294
+ id: "package-llm",
88295
+ family: "device-events",
88296
+ label: "Package confirm",
88297
+ kinds: ["package-llm-silent", "package-llm-answered"]
88298
+ },
87388
88299
  {
87389
88300
  id: "recording",
87390
88301
  family: "device-events",
@@ -87566,6 +88477,8 @@ var DEVICE_EVENTS = [
87566
88477
  "detection-blind",
87567
88478
  "motion-noisy",
87568
88479
  "motion-calm",
88480
+ "package-llm-silent",
88481
+ "package-llm-answered",
87569
88482
  "recording-stalled",
87570
88483
  "recording-healthy",
87571
88484
  "recording-degraded",
@@ -87834,6 +88747,15 @@ new Map([
87834
88747
  snapshotLabel: "Cameras with constant motion now",
87835
88748
  snapshotSection: null
87836
88749
  },
88750
+ {
88751
+ id: "package-llm-confirm",
88752
+ family: "device-events",
88753
+ label: "Package confirm (not answering / answering again)",
88754
+ bad: ["package-llm-silent"],
88755
+ recovery: ["package-llm-answered"],
88756
+ snapshotLabel: "Cameras whose package confirm is not answering",
88757
+ snapshotSection: null
88758
+ },
87837
88759
  {
87838
88760
  id: "recording-health",
87839
88761
  family: "device-events",
@@ -88410,6 +89332,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
88410
89332
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
88411
89333
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
88412
89334
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
89335
+ /**
89336
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
89337
+ * An unknown unit or a unit from another dimension is a refusal: the caller
89338
+ * logs it and publishes null (D393). This function never guesses.
89339
+ */
89340
+ var DIMENSION_QUANTITY = {
89341
+ temperature: "temperature",
89342
+ pressure: "pressure",
89343
+ speed: "speed",
89344
+ "precipitation-rate": "precipitation-rate",
89345
+ length: "length",
89346
+ area: "area",
89347
+ illuminance: "illuminance",
89348
+ irradiance: "irradiance",
89349
+ power: "power",
89350
+ "apparent-power": "apparent-power",
89351
+ energy: "energy",
89352
+ voltage: "voltage",
89353
+ current: "current",
89354
+ "concentration-volume": "concentration-volume",
89355
+ "concentration-mass": "concentration-mass",
89356
+ mass: "mass",
89357
+ percentage: "percentage",
89358
+ volume: "volume",
89359
+ duration: "duration",
89360
+ information: "information",
89361
+ angle: "angle",
89362
+ "sound-level": "sound-level",
89363
+ "signal-level": "signal-level",
89364
+ "colour-temperature": "colour-temperature",
89365
+ bitrate: "bitrate",
89366
+ rate: "rate"
89367
+ };
89368
+ /**
89369
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
89370
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
89371
+ * precipitation is chosen by the caller (a device class), not by the symbol.
89372
+ */
89373
+ function quantityFromUnit(rawUnit) {
89374
+ const unit = normalizeUnit(rawUnit);
89375
+ if (unit === void 0) return void 0;
89376
+ if (unit === "ppb") return "concentration-ppb";
89377
+ if (unit === "ms") return "duration-ms";
89378
+ const dimension = unitDimension(unit);
89379
+ if (dimension === void 0) return void 0;
89380
+ return DIMENSION_QUANTITY[dimension];
89381
+ }
89382
+ function ingestReading(input) {
89383
+ if (!Number.isFinite(input.value)) return {
89384
+ ok: false,
89385
+ reason: "non-finite"
89386
+ };
89387
+ if (input.quantity === "device") return {
89388
+ ok: false,
89389
+ reason: "unknown-unit"
89390
+ };
89391
+ const raw = input.rawUnit?.trim();
89392
+ if (raw === void 0 || raw.length === 0) return {
89393
+ ok: true,
89394
+ value: input.value
89395
+ };
89396
+ const unit = normalizeUnit(raw);
89397
+ if (unit === void 0) return {
89398
+ ok: false,
89399
+ reason: "unknown-unit"
89400
+ };
89401
+ const base = baseUnitOf(input.quantity);
89402
+ if (base === void 0) {
89403
+ if (unitDimension(unit) !== void 0) return {
89404
+ ok: false,
89405
+ reason: "cross-dimension"
89406
+ };
89407
+ return {
89408
+ ok: false,
89409
+ reason: "unknown-unit"
89410
+ };
89411
+ }
89412
+ const converted = tryConvertUnit(input.value, unit, base);
89413
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
89414
+ ok: false,
89415
+ reason: "unknown-unit"
89416
+ } : {
89417
+ ok: false,
89418
+ reason: "cross-dimension"
89419
+ };
89420
+ return {
89421
+ ok: true,
89422
+ value: converted
89423
+ };
89424
+ }
88413
89425
  var MB = 1024 * 1024;
88414
89426
  1024 * MB, 3072 * MB;
88415
89427
  //#endregion
@@ -191106,7 +192118,7 @@ var CONSUMABLES_COLD_START = {
191106
192118
  lastFetchedAt: 0
191107
192119
  };
191108
192120
  function itemEqual(a, b) {
191109
- return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable && a.kind === b.kind && a.remaining?.value === b.remaining?.value && a.remaining?.unit === b.remaining?.unit;
192121
+ return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable && a.kind === b.kind && a.remaining?.value === b.remaining?.value && a.remaining?.kind === b.remaining?.kind;
191110
192122
  }
191111
192123
  function itemsEqual(a, b) {
191112
192124
  return a.length === b.length && a.every((item, i) => {
@@ -191804,6 +192816,40 @@ var DreameMowerSubstateDevice = class extends DreameChildDevice {
191804
192816
  }
191805
192817
  };
191806
192818
  //#endregion
192819
+ //#region src/dreame-reading.ts
192820
+ /** Catalog unit → the quantity stored on the device. `times` is a count. */
192821
+ function dreameQuantity(unit) {
192822
+ if (unit === void 0 || unit.length === 0) return "none";
192823
+ if (unit === "times") return "count";
192824
+ const learned = quantityFromUnit(unit);
192825
+ if (learned === void 0 || learned === "device") return null;
192826
+ return learned;
192827
+ }
192828
+ /** Upstream number → base. A count ignores the word `times`. */
192829
+ function dreameToBase(value, unit, quantity) {
192830
+ const result = ingestReading({
192831
+ value,
192832
+ rawUnit: quantity === "count" ? void 0 : unit,
192833
+ quantity
192834
+ });
192835
+ return result.ok ? result.value : null;
192836
+ }
192837
+ /** Base number → the unit the robot still speaks. */
192838
+ function dreameToUpstream(value, unit, quantity) {
192839
+ const base = baseUnitOf(quantity);
192840
+ const upstream = unit === void 0 ? void 0 : normalizeUnit(unit);
192841
+ if (base === void 0 || upstream === void 0 || upstream === base) return value;
192842
+ return tryConvertUnit(value, base, upstream) ?? null;
192843
+ }
192844
+ /** A step is a difference, so an affine offset must not move it. */
192845
+ function dreameStep(step, unit, quantity) {
192846
+ if (step === void 0) return void 0;
192847
+ const base = baseUnitOf(quantity);
192848
+ const from = unit === void 0 ? void 0 : normalizeUnit(unit);
192849
+ if (base === void 0 || from === void 0 || from === base) return step;
192850
+ return tryConvertDelta(step, from, base);
192851
+ }
192852
+ //#endregion
191807
192853
  //#region src/devices/dreame-number-device.ts
191808
192854
  var CAP_NAME$5 = "control";
191809
192855
  /** Persisted config for a generic number child: the shared Dreame child config
@@ -191828,22 +192874,25 @@ var dreameNumberSchema = dreameChildSchema.extend({
191828
192874
  var DreameNumberDevice = class extends DreameChildDevice {
191829
192875
  #siid;
191830
192876
  #piid;
191831
- #min;
191832
- #max;
191833
- #step;
191834
192877
  #unit;
192878
+ #quantity;
192879
+ #baseMin;
192880
+ #baseMax;
192881
+ #baseStep;
191835
192882
  constructor(ctx) {
191836
192883
  super(ctx, DeviceRole.NumericControl);
191837
192884
  const cfg = dreameNumberSchema.parse(ctx.persistedConfig ?? {});
191838
192885
  this.#siid = cfg.siid;
191839
192886
  this.#piid = cfg.piid;
191840
- this.#min = cfg.min;
191841
- this.#max = cfg.max;
191842
- this.#step = cfg.step;
191843
192887
  this.#unit = cfg.unit;
192888
+ this.#quantity = dreameQuantity(cfg.unit);
192889
+ this.#baseMin = this.#toBase(cfg.min) ?? cfg.min;
192890
+ this.#baseMax = this.#toBase(cfg.max) ?? cfg.max;
192891
+ this.#baseStep = this.#quantity === null ? cfg.step : dreameStep(cfg.step, cfg.unit, this.#quantity);
191844
192892
  this.updateSourceInfo({
191845
192893
  id: `${this.dreameDeviceId}:number:${cfg.siid}-${cfg.piid}`,
191846
- system: "dreame"
192894
+ system: "dreame",
192895
+ ...this.#quantity !== null ? { quantity: this.#quantity } : {}
191847
192896
  });
191848
192897
  }
191849
192898
  /** Read the configured property's numeric value, or null when absent. */
@@ -191851,41 +192900,68 @@ var DreameNumberDevice = class extends DreameChildDevice {
191851
192900
  const v = this.resolveHandle()?.getProperty(this.#siid, this.#piid)?.value;
191852
192901
  return typeof v === "number" && Number.isFinite(v) ? v : null;
191853
192902
  }
191854
- #slice(value) {
192903
+ #toBase(value) {
192904
+ if (this.#quantity === null) return null;
192905
+ return dreameToBase(value, this.#unit, this.#quantity);
192906
+ }
192907
+ #slice(upstream) {
192908
+ const value = this.#toBase(upstream);
192909
+ if (value === null) {
192910
+ this.ctx.logger.warn("numeric reading refused", {
192911
+ tags: { deviceId: this.id },
192912
+ meta: {
192913
+ quantity: this.#quantity ?? "device",
192914
+ unit: this.#unit ?? "",
192915
+ reason: "unknown-unit"
192916
+ }
192917
+ });
192918
+ return null;
192919
+ }
191855
192920
  return {
191856
192921
  kind: "numeric",
191857
192922
  value,
191858
192923
  options: [],
191859
192924
  lastChangedAt: Date.now(),
191860
- min: this.#min,
191861
- max: this.#max,
191862
- step: this.#step,
191863
- ...this.#unit !== void 0 ? { unit: this.#unit } : {}
192925
+ min: this.#baseMin,
192926
+ max: this.#baseMax,
192927
+ ...this.#baseStep !== void 0 ? { step: this.#baseStep } : {}
191864
192928
  };
191865
192929
  }
191866
192930
  registerControlCap() {
191867
192931
  this.ctx.registerNativeCap(controlCapability, {
191868
- getStatus: async () => this.runtimeState.getCapState(CAP_NAME$5) ?? this.#slice(this.#read() ?? this.#min),
192932
+ getStatus: async () => this.runtimeState.getCapState(CAP_NAME$5) ?? {
192933
+ kind: "numeric",
192934
+ value: 0,
192935
+ options: [],
192936
+ lastChangedAt: 0
192937
+ },
191869
192938
  setValue: async ({ control }) => {
191870
192939
  if (control.kind !== "numeric") throw new Error(`DreameNumberDevice: expected a numeric value (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
192940
+ if (this.#quantity === null) throw new Error(`DreameNumberDevice: unknown unit (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
191871
192941
  const handle = this.resolveHandle();
191872
192942
  if (handle === null) throw new Error(`DreameNumberDevice: handle unavailable (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
191873
- const clamped = Math.min(this.#max, Math.max(this.#min, control.value));
192943
+ const upstream = dreameToUpstream(Math.min(this.#baseMax, Math.max(this.#baseMin, control.value)), this.#unit, this.#quantity);
192944
+ if (upstream === null) throw new Error(`DreameNumberDevice: cannot convert back (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
191874
192945
  await handle.setProperty({
191875
192946
  siid: this.#siid,
191876
192947
  piid: this.#piid,
191877
- value: clamped
192948
+ value: upstream
191878
192949
  });
191879
- this.runtimeState.setCapState(CAP_NAME$5, this.#slice(clamped));
192950
+ const slice = this.#slice(upstream);
192951
+ if (slice !== null) this.runtimeState.setCapState(CAP_NAME$5, slice);
191880
192952
  }
191881
192953
  });
191882
- this.runtimeState.setCapState(CAP_NAME$5, this.#slice(this.#read() ?? this.#min));
192954
+ const initial = this.#read();
192955
+ const slice = initial === null ? null : this.#slice(initial);
192956
+ if (slice !== null) this.runtimeState.setCapState(CAP_NAME$5, slice);
191883
192957
  }
191884
192958
  recomputeSlice() {
191885
192959
  const value = this.#read();
191886
192960
  if (value === null) return;
191887
- if (this.runtimeState.getCapState(CAP_NAME$5)?.value === value) return;
191888
- this.runtimeState.setCapState(CAP_NAME$5, this.#slice(value));
192961
+ const slice = this.#slice(value);
192962
+ if (slice === null) return;
192963
+ if (this.runtimeState.getCapState(CAP_NAME$5)?.value === slice.value) return;
192964
+ this.runtimeState.setCapState(CAP_NAME$5, slice);
191889
192965
  }
191890
192966
  };
191891
192967
  //#endregion
@@ -191916,9 +192992,11 @@ var DreameNumericSensorDevice = class extends DreameChildDevice {
191916
192992
  this.#siid = cfg.siid;
191917
192993
  this.#piid = cfg.piid;
191918
192994
  this.#unit = cfg.unit;
192995
+ const quantity = dreameQuantity(cfg.unit);
191919
192996
  this.updateSourceInfo({
191920
192997
  id: `${this.dreameDeviceId}:sensor:${cfg.siid}-${cfg.piid}`,
191921
- system: "dreame"
192998
+ system: "dreame",
192999
+ ...quantity !== null ? { quantity } : {}
191922
193000
  });
191923
193001
  }
191924
193002
  /** Read the configured property's numeric value from the shared handle, or null. */
@@ -191927,22 +193005,51 @@ var DreameNumericSensorDevice = class extends DreameChildDevice {
191927
193005
  return typeof v === "number" && Number.isFinite(v) ? v : null;
191928
193006
  }
191929
193007
  #slice(value) {
193008
+ const quantity = dreameQuantity(this.#unit);
193009
+ if (quantity === null) {
193010
+ this.ctx.logger.warn("numeric reading refused", {
193011
+ tags: { deviceId: this.id },
193012
+ meta: {
193013
+ quantity: "device",
193014
+ unit: this.#unit ?? "",
193015
+ reason: "unknown-unit"
193016
+ }
193017
+ });
193018
+ return null;
193019
+ }
193020
+ const stored = dreameToBase(value, this.#unit, quantity);
193021
+ if (stored === null) {
193022
+ this.ctx.logger.warn("numeric reading refused", {
193023
+ tags: { deviceId: this.id },
193024
+ meta: {
193025
+ quantity,
193026
+ unit: this.#unit ?? "",
193027
+ reason: "unknown-unit"
193028
+ }
193029
+ });
193030
+ return null;
193031
+ }
191930
193032
  return {
191931
- value,
191932
- ...this.#unit !== void 0 ? { unit: this.#unit } : {},
193033
+ value: stored,
191933
193034
  lastFetchedAt: Date.now()
191934
193035
  };
191935
193036
  }
191936
193037
  registerControlCap() {
191937
- this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(CAP_NAME$4) ?? this.#slice(0) });
193038
+ this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(CAP_NAME$4) ?? {
193039
+ value: 0,
193040
+ lastFetchedAt: 0
193041
+ } });
191938
193042
  const initial = this.#readValue();
191939
- this.runtimeState.setCapState(CAP_NAME$4, this.#slice(initial ?? 0));
193043
+ const slice = initial === null ? null : this.#slice(initial);
193044
+ if (slice !== null) this.runtimeState.setCapState(CAP_NAME$4, slice);
191940
193045
  }
191941
193046
  recomputeSlice() {
191942
193047
  const value = this.#readValue();
191943
193048
  if (value === null) return;
191944
- if (this.runtimeState.getCapState(CAP_NAME$4)?.value === value) return;
191945
- this.runtimeState.setCapState(CAP_NAME$4, this.#slice(value));
193049
+ const slice = this.#slice(value);
193050
+ if (slice === null) return;
193051
+ if (this.runtimeState.getCapState(CAP_NAME$4)?.value === slice.value) return;
193052
+ this.runtimeState.setCapState(CAP_NAME$4, slice);
191946
193053
  }
191947
193054
  };
191948
193055
  //#endregion