@camstack/addon-provider-bosch 0.1.24 → 0.1.26

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.
@@ -1,5 +1,5 @@
1
1
  require("node:crypto");
2
- //#region ../types/dist/event-category-DHXxB8t9.mjs
2
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -325,6 +325,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
325
325
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
326
326
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
327
327
  /**
328
+ * A camera's package LLM confirm has failed closed three times in a row
329
+ * (D931, 2026-10-26). One edge per camera until
330
+ * {@link PipelinePackageLlmAnswered}.
331
+ */
332
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
333
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
334
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
335
+ /**
328
336
  * Per-camera pipeline config was mutated by the orchestrator
329
337
  * (a settings change via `setAgentInferenceDevices` /
330
338
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5434,7 +5442,7 @@ var ZodIssueCode = {
5434
5442
  var ZodFirstPartyTypeKind;
5435
5443
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5436
5444
  //#endregion
5437
- //#region ../types/dist/sleep-BErAxhul.mjs
5445
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5438
5446
  /**
5439
5447
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5440
5448
  * window to float samples (D455).
@@ -7992,6 +8000,99 @@ var RecordKindDeclarationSchema = object({
7992
8000
  }).strict();
7993
8001
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7994
8002
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8003
+ var READING_QUANTITIES = [
8004
+ "temperature",
8005
+ "pressure",
8006
+ "speed",
8007
+ "precipitation",
8008
+ "precipitation-rate",
8009
+ "length",
8010
+ "area",
8011
+ "illuminance",
8012
+ "irradiance",
8013
+ "power",
8014
+ "energy",
8015
+ "voltage",
8016
+ "current",
8017
+ "apparent-power",
8018
+ "humidity",
8019
+ "percentage",
8020
+ "concentration-volume",
8021
+ "concentration-mass",
8022
+ "concentration-ppb",
8023
+ "volume",
8024
+ "duration",
8025
+ "duration-ms",
8026
+ "information",
8027
+ "sound-level",
8028
+ "signal-level",
8029
+ "colour-temperature",
8030
+ "angle",
8031
+ "bitrate",
8032
+ "rate",
8033
+ "mass",
8034
+ "index",
8035
+ "ratio",
8036
+ "count",
8037
+ "none",
8038
+ "device"
8039
+ ];
8040
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8041
+ new Set(READING_QUANTITIES);
8042
+ /**
8043
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8044
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8045
+ */
8046
+ var QUANTITY_BASE = {
8047
+ temperature: "°C",
8048
+ pressure: "hPa",
8049
+ speed: "m/s",
8050
+ precipitation: "mm",
8051
+ "precipitation-rate": "mm/h",
8052
+ length: "m",
8053
+ area: "m²",
8054
+ illuminance: "lx",
8055
+ irradiance: "W/m²",
8056
+ power: "W",
8057
+ energy: "kWh",
8058
+ voltage: "V",
8059
+ current: "A",
8060
+ "apparent-power": "VA",
8061
+ humidity: "%",
8062
+ percentage: "%",
8063
+ "concentration-volume": "ppm",
8064
+ "concentration-mass": "µg/m³",
8065
+ "concentration-ppb": "ppb",
8066
+ volume: "L",
8067
+ duration: "s",
8068
+ "duration-ms": "ms",
8069
+ information: "B",
8070
+ "sound-level": "dBFS",
8071
+ "signal-level": "dBm",
8072
+ "colour-temperature": "mired",
8073
+ angle: "°",
8074
+ bitrate: "kbps",
8075
+ rate: "fps",
8076
+ mass: "kg",
8077
+ index: "none",
8078
+ ratio: "none",
8079
+ count: "none",
8080
+ none: "none",
8081
+ device: "device"
8082
+ };
8083
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8084
+ function baseUnitOf(quantity) {
8085
+ const symbol = QUANTITY_BASE[quantity];
8086
+ if (symbol === "none" || symbol === "device") return void 0;
8087
+ return symbol;
8088
+ }
8089
+ function readingMeta(quantity, extras) {
8090
+ return {
8091
+ quantity,
8092
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8093
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8094
+ };
8095
+ }
7995
8096
  /** Shared Zod schemas used across streaming capabilities. */
7996
8097
  var CamProfileSchema = _enum([
7997
8098
  "high",
@@ -8019,8 +8120,8 @@ var CamStreamKindSchema = _enum([
8019
8120
  "derived"
8020
8121
  ]);
8021
8122
  var CamStreamResolutionSchema = object({
8022
- width: number().int().positive(),
8023
- height: number().int().positive()
8123
+ width: number().int().positive().meta(readingMeta("none")),
8124
+ height: number().int().positive().meta(readingMeta("none"))
8024
8125
  });
8025
8126
  var CameraStreamSchema = object({
8026
8127
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8560,6 +8661,8 @@ var CAP_NAMES = Object.freeze([
8560
8661
  "filesystem-browse",
8561
8662
  "flood",
8562
8663
  "gas",
8664
+ "group-commands",
8665
+ "group-status",
8563
8666
  "heating-valve",
8564
8667
  "humidifier",
8565
8668
  "humidity-sensor",
@@ -8689,14 +8792,6 @@ Object.freeze({
8689
8792
  {
8690
8793
  "path": "aqi",
8691
8794
  "valueType": "number"
8692
- },
8693
- {
8694
- "path": "unit",
8695
- "valueType": "string"
8696
- },
8697
- {
8698
- "path": "precision",
8699
- "valueType": "number"
8700
8795
  }
8701
8796
  ],
8702
8797
  "alarm-panel": [
@@ -8732,20 +8827,10 @@ Object.freeze({
8732
8827
  "valueType": "boolean"
8733
8828
  }
8734
8829
  ],
8735
- "ambient-light-sensor": [
8736
- {
8737
- "path": "lux",
8738
- "valueType": "number"
8739
- },
8740
- {
8741
- "path": "unit",
8742
- "valueType": "string"
8743
- },
8744
- {
8745
- "path": "precision",
8746
- "valueType": "number"
8747
- }
8748
- ],
8830
+ "ambient-light-sensor": [{
8831
+ "path": "lux",
8832
+ "valueType": "number"
8833
+ }],
8749
8834
  "audio-metrics": [
8750
8835
  {
8751
8836
  "path": "ts",
@@ -8825,6 +8910,10 @@ Object.freeze({
8825
8910
  "path": "lastUpdated",
8826
8911
  "valueType": "number"
8827
8912
  },
8913
+ {
8914
+ "path": "volts",
8915
+ "valueType": "number"
8916
+ },
8828
8917
  {
8829
8918
  "path": "lastContactAt",
8830
8919
  "valueType": "number"
@@ -9067,10 +9156,6 @@ Object.freeze({
9067
9156
  "datetime"
9068
9157
  ]
9069
9158
  },
9070
- {
9071
- "path": "unit",
9072
- "valueType": "string"
9073
- },
9074
9159
  {
9075
9160
  "path": "min",
9076
9161
  "valueType": "number"
@@ -9082,19 +9167,6 @@ Object.freeze({
9082
9167
  {
9083
9168
  "path": "step",
9084
9169
  "valueType": "number"
9085
- },
9086
- {
9087
- "path": "precision",
9088
- "valueType": "number"
9089
- },
9090
- {
9091
- "path": "format",
9092
- "valueType": "enum",
9093
- "enumValues": [
9094
- "date",
9095
- "time",
9096
- "datetime"
9097
- ]
9098
9170
  }
9099
9171
  ],
9100
9172
  "cover": [
@@ -9335,13 +9407,8 @@ Object.freeze({
9335
9407
  "path": "value",
9336
9408
  "valueType": "string"
9337
9409
  }, {
9338
- "path": "format",
9339
- "valueType": "enum",
9340
- "enumValues": [
9341
- "date",
9342
- "time",
9343
- "datetime"
9344
- ]
9410
+ "path": "timestamp",
9411
+ "valueType": "boolean"
9345
9412
  }],
9346
9413
  "event-emitter": [
9347
9414
  {
@@ -9414,6 +9481,20 @@ Object.freeze({
9414
9481
  "path": "detected",
9415
9482
  "valueType": "boolean"
9416
9483
  }],
9484
+ "group-status": [
9485
+ {
9486
+ "path": "included",
9487
+ "valueType": "number"
9488
+ },
9489
+ {
9490
+ "path": "excluded",
9491
+ "valueType": "number"
9492
+ },
9493
+ {
9494
+ "path": "total",
9495
+ "valueType": "number"
9496
+ }
9497
+ ],
9417
9498
  "heating-valve": [{
9418
9499
  "path": "position",
9419
9500
  "valueType": "number"
@@ -9448,20 +9529,10 @@ Object.freeze({
9448
9529
  "valueType": "number"
9449
9530
  }
9450
9531
  ],
9451
- "humidity-sensor": [
9452
- {
9453
- "path": "percent",
9454
- "valueType": "number"
9455
- },
9456
- {
9457
- "path": "unit",
9458
- "valueType": "string"
9459
- },
9460
- {
9461
- "path": "precision",
9462
- "valueType": "number"
9463
- }
9464
- ],
9532
+ "humidity-sensor": [{
9533
+ "path": "percent",
9534
+ "valueType": "number"
9535
+ }],
9465
9536
  "image": [{
9466
9537
  "path": "url",
9467
9538
  "valueType": "string"
@@ -9800,20 +9871,10 @@ Object.freeze({
9800
9871
  "valueType": "boolean"
9801
9872
  }
9802
9873
  ],
9803
- "numeric-sensor": [
9804
- {
9805
- "path": "value",
9806
- "valueType": "number"
9807
- },
9808
- {
9809
- "path": "unit",
9810
- "valueType": "string"
9811
- },
9812
- {
9813
- "path": "precision",
9814
- "valueType": "number"
9815
- }
9816
- ],
9874
+ "numeric-sensor": [{
9875
+ "path": "value",
9876
+ "valueType": "number"
9877
+ }],
9817
9878
  "pet-feeder": [
9818
9879
  {
9819
9880
  "path": "foodLevel",
@@ -9905,14 +9966,6 @@ Object.freeze({
9905
9966
  {
9906
9967
  "path": "powerFactor",
9907
9968
  "valueType": "number"
9908
- },
9909
- {
9910
- "path": "unit",
9911
- "valueType": "string"
9912
- },
9913
- {
9914
- "path": "precision",
9915
- "valueType": "number"
9916
9969
  }
9917
9970
  ],
9918
9971
  "presence": [
@@ -9941,20 +9994,10 @@ Object.freeze({
9941
9994
  "valueType": "number"
9942
9995
  }
9943
9996
  ],
9944
- "pressure-sensor": [
9945
- {
9946
- "path": "hpa",
9947
- "valueType": "number"
9948
- },
9949
- {
9950
- "path": "unit",
9951
- "valueType": "string"
9952
- },
9953
- {
9954
- "path": "precision",
9955
- "valueType": "number"
9956
- }
9957
- ],
9997
+ "pressure-sensor": [{
9998
+ "path": "hpa",
9999
+ "valueType": "number"
10000
+ }],
9958
10001
  "privacy-mask": [{
9959
10002
  "path": "enabled",
9960
10003
  "valueType": "boolean"
@@ -10265,20 +10308,10 @@ Object.freeze({
10265
10308
  "path": "tampered",
10266
10309
  "valueType": "boolean"
10267
10310
  }],
10268
- "temperature-sensor": [
10269
- {
10270
- "path": "celsius",
10271
- "valueType": "number"
10272
- },
10273
- {
10274
- "path": "unit",
10275
- "valueType": "string"
10276
- },
10277
- {
10278
- "path": "precision",
10279
- "valueType": "number"
10280
- }
10281
- ],
10311
+ "temperature-sensor": [{
10312
+ "path": "celsius",
10313
+ "valueType": "number"
10314
+ }],
10282
10315
  "update": [
10283
10316
  {
10284
10317
  "path": "currentVersion",
@@ -10389,10 +10422,6 @@ Object.freeze({
10389
10422
  "path": "temperature",
10390
10423
  "valueType": "number"
10391
10424
  },
10392
- {
10393
- "path": "temperatureUnit",
10394
- "valueType": "string"
10395
- },
10396
10425
  {
10397
10426
  "path": "humidity",
10398
10427
  "valueType": "number"
@@ -10401,18 +10430,10 @@ Object.freeze({
10401
10430
  "path": "pressure",
10402
10431
  "valueType": "number"
10403
10432
  },
10404
- {
10405
- "path": "pressureUnit",
10406
- "valueType": "string"
10407
- },
10408
10433
  {
10409
10434
  "path": "windSpeed",
10410
10435
  "valueType": "number"
10411
10436
  },
10412
- {
10413
- "path": "windSpeedUnit",
10414
- "valueType": "string"
10415
- },
10416
10437
  {
10417
10438
  "path": "windBearing",
10418
10439
  "valueType": "number"
@@ -10815,7 +10836,7 @@ function errMsg(err) {
10815
10836
  return String(err);
10816
10837
  }
10817
10838
  //#endregion
10818
- //#region ../types/dist/composition-kEl9mevu.mjs
10839
+ //#region ../types/dist/composition-OhpuespA.mjs
10819
10840
  /**
10820
10841
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10821
10842
  *
@@ -11570,15 +11591,74 @@ function validateExpressionSource(src) {
11570
11591
  }
11571
11592
  return null;
11572
11593
  }
11573
- /** A composed device's stableId is this prefix plus the block id. */
11574
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11575
- /** Longest snippet a `code` source may carry (slice 4). */
11576
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11594
+ /**
11595
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11596
+ * id. Split out so helper schemas can name it without a cycle through
11597
+ * `composition.ts` (D659).
11598
+ */
11577
11599
  var CompositionSourceRefSchema = object({
11578
11600
  /** The owning addon; `stableId` is unique only within it. */
11579
11601
  addonId: string().min(1),
11580
11602
  stableId: string().min(1)
11581
11603
  });
11604
+ var GROUP_TEMPLATE_IDS = [
11605
+ "temperature",
11606
+ "humidity",
11607
+ "pressure",
11608
+ "illuminance",
11609
+ "power",
11610
+ "energy",
11611
+ "battery",
11612
+ "numeric",
11613
+ "motion",
11614
+ "contact",
11615
+ "leak",
11616
+ "smoke",
11617
+ "occupancy",
11618
+ "binary",
11619
+ "switch",
11620
+ "light"
11621
+ ];
11622
+ var NUMERIC_AGGREGATES = [
11623
+ "mean",
11624
+ "median",
11625
+ "min",
11626
+ "max",
11627
+ "sum"
11628
+ ];
11629
+ var BINARY_AGGREGATES = ["any", "all"];
11630
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11631
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11632
+ var AGGREGATE_REDUCERS = [
11633
+ "mean",
11634
+ "median",
11635
+ "min",
11636
+ "max",
11637
+ "sum",
11638
+ "any",
11639
+ "all"
11640
+ ];
11641
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11642
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11643
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11644
+ /** One member. The field is stored so occupancy can name motion or binary. */
11645
+ var HelperMemberSchema = object({
11646
+ source: CompositionSourceRefSchema,
11647
+ cap: string().min(1),
11648
+ fieldPath: string().min(1)
11649
+ });
11650
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11651
+ kind: literal("group"),
11652
+ templateId: GroupTemplateIdSchema,
11653
+ aggregate: AggregateReducerSchema,
11654
+ members: array(HelperMemberSchema).min(1).max(64),
11655
+ /** Actuator groups only. Readings leave this unset. */
11656
+ active: ActiveCriterionSchema.optional()
11657
+ })]);
11658
+ /** A composed device's stableId is this prefix plus the block id. */
11659
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11660
+ /** Longest snippet a `code` source may carry (slice 4). */
11661
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11582
11662
  /** One field of one capability of one source device. */
11583
11663
  var CompositionFieldReadSchema = object({
11584
11664
  source: CompositionSourceRefSchema,
@@ -11622,7 +11702,33 @@ var CompositionCodeSourceSchema = object({
11622
11702
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11623
11703
  CompositionFromSourceSchema,
11624
11704
  CompositionExpressionSourceSchema,
11625
- CompositionCodeSourceSchema
11705
+ CompositionCodeSourceSchema,
11706
+ object({
11707
+ kind: literal("aggregate"),
11708
+ reducer: _enum([
11709
+ "mean",
11710
+ "median",
11711
+ "min",
11712
+ "max",
11713
+ "sum",
11714
+ "any",
11715
+ "all"
11716
+ ]),
11717
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11718
+ /** `value` is the reading; the others are the member counts. */
11719
+ publish: _enum([
11720
+ "value",
11721
+ "included",
11722
+ "excluded",
11723
+ "total"
11724
+ ]),
11725
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11726
+ empty: _enum([
11727
+ "null",
11728
+ "omit",
11729
+ "offline"
11730
+ ])
11731
+ })
11626
11732
  ]);
11627
11733
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11628
11734
  kind: literal("forward"),
@@ -11668,7 +11774,13 @@ var CompositionSchema = object({
11668
11774
  kind: literal("existing"),
11669
11775
  device: CompositionSourceRefSchema
11670
11776
  })]),
11671
- features: array(CompositionFeatureSchema).min(1).max(16)
11777
+ features: array(CompositionFeatureSchema).min(1).max(16),
11778
+ /**
11779
+ * Present when the block is a helper template. Absent on a free
11780
+ * composition (the room example, device 4603). The editor reads this,
11781
+ * never the compiled features.
11782
+ */
11783
+ helper: HelperSpecSchema.optional()
11672
11784
  }).superRefine((composition, ctx) => {
11673
11785
  const seen = /* @__PURE__ */ new Set();
11674
11786
  composition.features.forEach((feature, index) => {
@@ -17876,7 +17988,11 @@ var CompositionProblemSchema = object({
17876
17988
  "item-field-reserved",
17877
17989
  "target-missing",
17878
17990
  "target-unreadable",
17879
- "not-replaceable"
17991
+ "not-replaceable",
17992
+ "native-capability-replace",
17993
+ "mixed-quantity",
17994
+ "helper-mismatch",
17995
+ "member-constraint"
17880
17996
  ]),
17881
17997
  path: string(),
17882
17998
  message: string()
@@ -17897,7 +18013,8 @@ var CompositionUnavailableModeSchema = _enum([
17897
18013
  "null",
17898
18014
  "offline",
17899
18015
  "unknown",
17900
- "release"
18016
+ "release",
18017
+ "omit"
17901
18018
  ]);
17902
18019
  /**
17903
18020
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17934,9 +18051,11 @@ var CompositionFieldPlanSchema = object({
17934
18051
  reads: array(CompositionFieldReadSchema)
17935
18052
  });
17936
18053
  /**
17937
- * What a feature does to its target (D663): `new` builds a capability on a new
17938
- * device; on an existing device it `add`s a capability the device lacks, or
17939
- * `replace`s named fields of one it has natively.
18054
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18055
+ * new device; on an existing device it `add`s a capability the device lacks.
18056
+ * Replacing a capability the device already serves is refused
18057
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18058
+ * wire for a claim the hub already holds.
17940
18059
  */
17941
18060
  var CompositionFeatureModeSchema = _enum([
17942
18061
  "new",
@@ -18109,6 +18228,275 @@ Object.freeze({
18109
18228
  result: ["number", "null"]
18110
18229
  }
18111
18230
  });
18231
+ /**
18232
+ * One catalog row per base reading a group helper can build (D944 §3).
18233
+ * Data, not a form default written into the block.
18234
+ */
18235
+ var NOW = {
18236
+ fieldPath: "lastFetchedAt",
18237
+ expr: "now"
18238
+ };
18239
+ var CHANGED = {
18240
+ fieldPath: "lastChangedAt",
18241
+ expr: "now"
18242
+ };
18243
+ function numeric(input) {
18244
+ return {
18245
+ ...input,
18246
+ family: "numeric",
18247
+ deviceType: DeviceType.Sensor,
18248
+ memberDeviceType: DeviceType.Sensor,
18249
+ commandCaps: [],
18250
+ baseUnit: baseUnitOf(input.quantity),
18251
+ aggregates: NUMERIC_AGGREGATES,
18252
+ accept: [{
18253
+ cap: input.outputCap,
18254
+ fieldPath: input.outputField
18255
+ }]
18256
+ };
18257
+ }
18258
+ function binary(input) {
18259
+ return {
18260
+ ...input,
18261
+ family: "binary",
18262
+ deviceType: DeviceType.Sensor,
18263
+ memberDeviceType: DeviceType.Sensor,
18264
+ commandCaps: [],
18265
+ quantity: "none",
18266
+ baseUnit: void 0,
18267
+ aggregates: BINARY_AGGREGATES,
18268
+ empty: "offline"
18269
+ };
18270
+ }
18271
+ var GROUP_TEMPLATES = [
18272
+ numeric({
18273
+ id: "temperature",
18274
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18275
+ role: DeviceRole.TemperatureSensor,
18276
+ outputCap: "temperature-sensor",
18277
+ outputField: "celsius",
18278
+ quantity: "temperature",
18279
+ defaultAggregate: "mean",
18280
+ empty: "null",
18281
+ companions: [NOW]
18282
+ }),
18283
+ numeric({
18284
+ id: "humidity",
18285
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18286
+ role: DeviceRole.HumiditySensor,
18287
+ outputCap: "humidity-sensor",
18288
+ outputField: "percent",
18289
+ quantity: "humidity",
18290
+ defaultAggregate: "mean",
18291
+ empty: "null",
18292
+ companions: [NOW]
18293
+ }),
18294
+ numeric({
18295
+ id: "pressure",
18296
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18297
+ role: DeviceRole.PressureSensor,
18298
+ outputCap: "pressure-sensor",
18299
+ outputField: "hpa",
18300
+ quantity: "pressure",
18301
+ defaultAggregate: "mean",
18302
+ empty: "null",
18303
+ companions: [NOW]
18304
+ }),
18305
+ numeric({
18306
+ id: "illuminance",
18307
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18308
+ role: DeviceRole.AmbientLightSensor,
18309
+ outputCap: "ambient-light-sensor",
18310
+ outputField: "lux",
18311
+ quantity: "illuminance",
18312
+ defaultAggregate: "mean",
18313
+ empty: "null",
18314
+ companions: [NOW]
18315
+ }),
18316
+ numeric({
18317
+ id: "power",
18318
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18319
+ role: DeviceRole.PowerSensor,
18320
+ outputCap: "power-meter",
18321
+ outputField: "watts",
18322
+ quantity: "power",
18323
+ defaultAggregate: "sum",
18324
+ empty: "omit",
18325
+ companions: [NOW]
18326
+ }),
18327
+ numeric({
18328
+ id: "energy",
18329
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18330
+ role: DeviceRole.EnergySensor,
18331
+ outputCap: "power-meter",
18332
+ outputField: "kwhTotal",
18333
+ quantity: "energy",
18334
+ defaultAggregate: "sum",
18335
+ empty: "omit",
18336
+ companions: [NOW]
18337
+ }),
18338
+ numeric({
18339
+ id: "battery",
18340
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18341
+ role: DeviceRole.BatterySensor,
18342
+ outputCap: "battery",
18343
+ outputField: "percentage",
18344
+ quantity: "percentage",
18345
+ defaultAggregate: "mean",
18346
+ empty: "null",
18347
+ companions: [
18348
+ {
18349
+ fieldPath: "charging",
18350
+ expr: "\"unknown\""
18351
+ },
18352
+ {
18353
+ fieldPath: "sleeping",
18354
+ expr: "false"
18355
+ },
18356
+ {
18357
+ fieldPath: "lastUpdated",
18358
+ expr: "now"
18359
+ }
18360
+ ]
18361
+ }),
18362
+ numeric({
18363
+ id: "numeric",
18364
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18365
+ role: DeviceRole.NumericSensor,
18366
+ outputCap: "numeric-sensor",
18367
+ outputField: "value",
18368
+ quantity: "device",
18369
+ defaultAggregate: "mean",
18370
+ empty: "null",
18371
+ companions: [NOW]
18372
+ }),
18373
+ binary({
18374
+ id: "motion",
18375
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18376
+ role: DeviceRole.MotionSensor,
18377
+ outputCap: "motion",
18378
+ outputField: "detected",
18379
+ defaultAggregate: "any",
18380
+ accept: [{
18381
+ cap: "motion",
18382
+ fieldPath: "detected"
18383
+ }],
18384
+ companions: [{
18385
+ fieldPath: "lastDetectedAt",
18386
+ expr: "null"
18387
+ }, {
18388
+ fieldPath: "autoClearAfterMs",
18389
+ expr: "null"
18390
+ }]
18391
+ }),
18392
+ binary({
18393
+ id: "contact",
18394
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18395
+ role: DeviceRole.ContactSensor,
18396
+ outputCap: "contact",
18397
+ outputField: "entryOpen",
18398
+ defaultAggregate: "any",
18399
+ accept: [{
18400
+ cap: "contact",
18401
+ fieldPath: "entryOpen"
18402
+ }],
18403
+ companions: [CHANGED]
18404
+ }),
18405
+ binary({
18406
+ id: "leak",
18407
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18408
+ role: DeviceRole.LeakSensor,
18409
+ outputCap: "flood",
18410
+ outputField: "flooded",
18411
+ defaultAggregate: "any",
18412
+ accept: [{
18413
+ cap: "flood",
18414
+ fieldPath: "flooded"
18415
+ }],
18416
+ companions: [CHANGED]
18417
+ }),
18418
+ binary({
18419
+ id: "smoke",
18420
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18421
+ role: DeviceRole.SmokeSensor,
18422
+ outputCap: "smoke",
18423
+ outputField: "detected",
18424
+ defaultAggregate: "any",
18425
+ accept: [{
18426
+ cap: "smoke",
18427
+ fieldPath: "detected"
18428
+ }],
18429
+ companions: [CHANGED]
18430
+ }),
18431
+ binary({
18432
+ id: "occupancy",
18433
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18434
+ role: DeviceRole.BinarySensor,
18435
+ outputCap: "binary",
18436
+ outputField: "on",
18437
+ defaultAggregate: "any",
18438
+ accept: [{
18439
+ cap: "motion",
18440
+ fieldPath: "detected"
18441
+ }, {
18442
+ cap: "binary",
18443
+ fieldPath: "on"
18444
+ }],
18445
+ companions: [CHANGED]
18446
+ }),
18447
+ binary({
18448
+ id: "binary",
18449
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18450
+ role: DeviceRole.BinarySensor,
18451
+ outputCap: "binary",
18452
+ outputField: "on",
18453
+ defaultAggregate: "any",
18454
+ accept: [{
18455
+ cap: "binary",
18456
+ fieldPath: "on"
18457
+ }],
18458
+ companions: [CHANGED]
18459
+ }),
18460
+ actuator({
18461
+ id: "switch",
18462
+ 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.",
18463
+ deviceType: DeviceType.Switch,
18464
+ outputCap: "switch",
18465
+ commandCaps: ["switch"]
18466
+ }),
18467
+ actuator({
18468
+ id: "light",
18469
+ 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.",
18470
+ deviceType: DeviceType.Light,
18471
+ outputCap: "switch",
18472
+ commandCaps: [
18473
+ "switch",
18474
+ "brightness",
18475
+ "color"
18476
+ ]
18477
+ })
18478
+ ];
18479
+ function actuator(input) {
18480
+ return {
18481
+ ...input,
18482
+ family: "actuator",
18483
+ role: void 0,
18484
+ memberDeviceType: input.deviceType,
18485
+ outputField: "on",
18486
+ quantity: "none",
18487
+ baseUnit: void 0,
18488
+ aggregates: BINARY_AGGREGATES,
18489
+ defaultAggregate: "any",
18490
+ empty: "offline",
18491
+ accept: [{
18492
+ cap: "switch",
18493
+ fieldPath: "on"
18494
+ }],
18495
+ companions: [CHANGED]
18496
+ };
18497
+ }
18498
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18499
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18112
18500
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18113
18501
  /**
18114
18502
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19822,12 +20210,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19822
20210
  * providers detect renames by matching `uniqueId` and patch `id`
19823
20211
  * in-place so dispatch keeps working.
19824
20212
  *
19825
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19826
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19827
- * state push and are the single source of truth. The `device_class` tag is
19828
- * an HA-specific input that is consumed at the discovery boundary
19829
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19830
- * stored after that point.
20213
+ * **Display unit and precision are NOT stored here.** They are a read-time
20214
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20215
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20216
+ * neither is repeated on each sample. The `device_class` tag is an
20217
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20218
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19831
20219
  *
19832
20220
  * The `raw` escape hatch carries provider-specific attributes that don't
19833
20221
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19841,6 +20229,14 @@ var SourceInfoSchema = object({
19841
20229
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19842
20230
  * Used to detect rename when `id` changes. */
19843
20231
  uniqueId: string().optional(),
20232
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20233
+ quantity: ReadingQuantitySchema.optional(),
20234
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20235
+ dateTimeShape: _enum([
20236
+ "date",
20237
+ "time",
20238
+ "datetime"
20239
+ ]).optional(),
19844
20240
  /** Provider-specific extras that don't fit the structured slots.
19845
20241
  * Whitelist what goes here per provider — do NOT dump entire HA
19846
20242
  * attribute blobs (would explode the meta JSON column). */
@@ -19886,6 +20282,80 @@ function mergeSourceInfo(prev, patch) {
19886
20282
  return next;
19887
20283
  }
19888
20284
  /**
20285
+ * Coerce common unit-of-measurement string variants to a canonical form
20286
+ * so the UI's display templates can rely on stable strings. Pure
20287
+ * passthrough for unknown units — kept conservative on purpose, the goal
20288
+ * is to normalise the long tail of HA `unit_of_measurement` values that
20289
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
20290
+ *
20291
+ * Extend the map as we encounter real-world data. Returning the raw
20292
+ * input for unknown values is intentional: providers should never
20293
+ * lose information silently here.
20294
+ */
20295
+ function normalizeUnit(raw) {
20296
+ if (raw == null) return void 0;
20297
+ const trimmed = raw.trim();
20298
+ if (trimmed === "") return void 0;
20299
+ const lower = trimmed.toLowerCase();
20300
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
20301
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
20302
+ if (lower === "k" || lower === "kelvin") return "K";
20303
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
20304
+ if (lower === "lx" || lower === "lux") return "lx";
20305
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
20306
+ if (lower === "kpa") return "kPa";
20307
+ if (lower === "pa") return "Pa";
20308
+ if (lower === "mbar" || lower === "millibar") return "mbar";
20309
+ if (lower === "w" || lower === "watt") return "W";
20310
+ if (lower === "kw" || lower === "kilowatt") return "kW";
20311
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
20312
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
20313
+ if (lower === "v" || lower === "volt") return "V";
20314
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
20315
+ if (lower === "ma" || lower === "milliampere") return "mA";
20316
+ if (lower === "va") return "VA";
20317
+ if (lower === "ppm") return "ppm";
20318
+ if (lower === "ppb") return "ppb";
20319
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
20320
+ if (lower === "km/h" || lower === "kph") return "km/h";
20321
+ if (lower === "mph") return "mph";
20322
+ if (lower === "m/s") return "m/s";
20323
+ if (lower === "m") return "m";
20324
+ if (lower === "m²" || lower === "m2") return "m²";
20325
+ if (lower === "mm") return "mm";
20326
+ if (lower === "cm") return "cm";
20327
+ if (lower === "km") return "km";
20328
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
20329
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
20330
+ if (lower === "kg") return "kg";
20331
+ if (lower === "g") return "g";
20332
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
20333
+ if (lower === "ml") return "mL";
20334
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
20335
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
20336
+ if (lower === "ms") return "ms";
20337
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
20338
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
20339
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
20340
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
20341
+ if (lower === "kib") return "KiB";
20342
+ if (lower === "mib") return "MiB";
20343
+ if (lower === "gib") return "GiB";
20344
+ if (lower === "tib") return "TiB";
20345
+ if (lower === "kb") return "kB";
20346
+ if (lower === "mb") return "MB";
20347
+ if (lower === "gb") return "GB";
20348
+ if (lower === "dbm") return "dBm";
20349
+ if (lower === "dbfs") return "dBFS";
20350
+ if (lower === "inhg") return "inHg";
20351
+ if (lower === "mm/h") return "mm/h";
20352
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
20353
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
20354
+ if (lower === "kbps") return "kbps";
20355
+ if (lower === "fps") return "fps";
20356
+ return trimmed;
20357
+ }
20358
+ /**
19889
20359
  * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
19890
20360
  * blob is a free-form `Record<string, unknown>` shared with hardware-
19891
20361
  * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
@@ -40962,7 +41432,8 @@ method(object({
40962
41432
  "enum"
40963
41433
  ]),
40964
41434
  enumValues: array(string()).optional(),
40965
- item: boolean().optional()
41435
+ item: boolean().optional(),
41436
+ quantity: ReadingQuantitySchema.optional()
40966
41437
  })).readonly(),
40967
41438
  itemArray: object({
40968
41439
  path: string(),
@@ -44806,8 +45277,8 @@ var MaskPolygonShapeSchema = object({
44806
45277
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44807
45278
  var MaskGridShapeSchema = object({
44808
45279
  kind: literal("grid"),
44809
- gridWidth: number(),
44810
- gridHeight: number(),
45280
+ gridWidth: number().meta(readingMeta("none")),
45281
+ gridHeight: number().meta(readingMeta("none")),
44811
45282
  cells: array(boolean())
44812
45283
  });
44813
45284
  discriminatedUnion("kind", [
@@ -44919,6 +45390,8 @@ var NcSystemEventKindSchema = _enum([
44919
45390
  "detection-blind",
44920
45391
  "motion-noisy",
44921
45392
  "motion-calm",
45393
+ "package-llm-silent",
45394
+ "package-llm-answered",
44922
45395
  "recording-stalled",
44923
45396
  "recording-healthy",
44924
45397
  "recording-degraded",
@@ -56010,6 +56483,28 @@ var PackageVersionInfoSchema = object({
56010
56483
  deprecated: string().optional(),
56011
56484
  distTags: array(string()).readonly()
56012
56485
  });
56486
+ /**
56487
+ * One published document. `ok` is markdown from the registry. `missing` means
56488
+ * the registry answered and the file was not there. `error` is a registry or
56489
+ * tarball failure — never an empty string standing in for either.
56490
+ */
56491
+ var PackageDocBodySchema = discriminatedUnion("status", [
56492
+ object({
56493
+ status: literal("ok"),
56494
+ markdown: string()
56495
+ }),
56496
+ object({ status: literal("missing") }),
56497
+ object({
56498
+ status: literal("error"),
56499
+ message: string()
56500
+ })
56501
+ ]);
56502
+ var PackageDocsSchema = object({
56503
+ name: string(),
56504
+ version: string(),
56505
+ readme: PackageDocBodySchema,
56506
+ changelog: PackageDocBodySchema
56507
+ });
56013
56508
  var SearchResultSchema = object({
56014
56509
  name: string(),
56015
56510
  version: string(),
@@ -56228,7 +56723,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56228
56723
  }), object({ success: literal(true) }), {
56229
56724
  kind: "mutation",
56230
56725
  auth: "admin"
56231
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56726
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56727
+ name: string().min(1),
56728
+ version: string().min(1).optional()
56729
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56232
56730
  kind: "mutation",
56233
56731
  auth: "admin"
56234
56732
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56287,27 +56785,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56287
56785
  */
56288
56786
  var AirQualitySensorStatusSchema = object({
56289
56787
  /** Carbon dioxide concentration in ppm. */
56290
- co2Ppm: number().min(0).optional(),
56788
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56291
56789
  /** Total volatile organic compounds in ppb. */
56292
- vocPpb: number().min(0).optional(),
56790
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56293
56791
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56294
- pm25: number().min(0).optional(),
56792
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56295
56793
  /** Particulate matter ≤ 10 μm in µg/m³. */
56296
- pm10: number().min(0).optional(),
56794
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56297
56795
  /** Composite AQI value (typically 0..500). */
56298
- aqi: number().optional(),
56796
+ aqi: number().optional().meta(readingMeta("index")),
56299
56797
  /** Ms epoch when the slice was last updated. */
56300
- lastFetchedAt: number(),
56301
- /** Live display unit of the single metric this slice carries (e.g. HA
56302
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56303
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56304
- * per slice is unambiguous. */
56305
- unit: string().optional(),
56306
- /** Suggested decimal places for numeric display.
56307
- * Populated live from the upstream source when provided (e.g. HA
56308
- * `attributes.suggested_display_precision`). Falls back to
56309
- * auto-formatting when absent. */
56310
- precision: number().int().min(0).max(10).optional()
56798
+ lastFetchedAt: number()
56311
56799
  });
56312
56800
  var airQualitySensorCapability = {
56313
56801
  name: "air-quality-sensor",
@@ -56344,12 +56832,12 @@ var airQualitySensorCapability = {
56344
56832
  },
56345
56833
  pm25: {
56346
56834
  kind: "gauge",
56347
- unit: "ug/m3",
56835
+ unit: "µg/m³",
56348
56836
  deadband: 1
56349
56837
  },
56350
56838
  pm10: {
56351
56839
  kind: "gauge",
56352
- unit: "ug/m3",
56840
+ unit: "µg/m³",
56353
56841
  deadband: 1
56354
56842
  },
56355
56843
  aqi: {
@@ -56367,22 +56855,13 @@ var airQualitySensorCapability = {
56367
56855
  * entries with `device_class: illuminance`.
56368
56856
  */
56369
56857
  var AmbientLightSensorStatusSchema = object({
56370
- /** Current illuminance in lux (lx). */
56371
- lux: number().min(0).meta({
56372
- unit: "lx",
56858
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56859
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56860
+ precision: 0,
56373
56861
  label: "Illuminance"
56374
- }),
56862
+ })),
56375
56863
  /** Ms epoch when the slice was last updated. */
56376
- lastFetchedAt: number(),
56377
- /** Live display unit from the upstream source (e.g. HA
56378
- * `attributes.unit_of_measurement`). The UI prefers this over the
56379
- * role's canonical unit. Absent → fall back to the canonical unit. */
56380
- unit: string().optional(),
56381
- /** Suggested decimal places for numeric display.
56382
- * Populated live from the upstream source when provided (e.g. HA
56383
- * `attributes.suggested_display_precision`). Falls back to
56384
- * auto-formatting when absent. */
56385
- precision: number().int().min(0).max(10).optional()
56864
+ lastFetchedAt: number()
56386
56865
  });
56387
56866
  var ambientLightSensorCapability = {
56388
56867
  name: "ambient-light-sensor",
@@ -56414,12 +56893,7 @@ var ambientLightSensorCapability = {
56414
56893
  } } },
56415
56894
  /** Clock fields: written, but excluded from the compare that decides
56416
56895
  * whether persisting is worth a SQLite commit. */
56417
- volatileStateFields: ["lastFetchedAt"],
56418
- customize: {
56419
- version: 1,
56420
- advanced: "derive",
56421
- targets: []
56422
- }
56896
+ volatileStateFields: ["lastFetchedAt"]
56423
56897
  };
56424
56898
  /**
56425
56899
  * Per-class audio metrics aggregated over a sliding window.
@@ -56646,10 +57120,7 @@ var BatteryStatusSchema = object({
56646
57120
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56647
57121
  * declines to band a non-finite reading, which is the correct shape.
56648
57122
  */
56649
- percentage: number().min(0).max(100).nullable().meta({
56650
- unit: "%",
56651
- label: "Charge"
56652
- }),
57123
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56653
57124
  /**
56654
57125
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56655
57126
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56676,7 +57147,14 @@ var BatteryStatusSchema = object({
56676
57147
  */
56677
57148
  sleeping: boolean().meta({ label: "Sleeping" }),
56678
57149
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56679
- lastUpdated: number(),
57150
+ lastUpdated: number().meta(readingMeta("none")),
57151
+ /**
57152
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57153
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57154
+ * that only report charge. A second reading on this cap, not a second
57155
+ * `numeric-sensor` on the same device.
57156
+ */
57157
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56680
57158
  /**
56681
57159
  * Ms epoch of the last time the device PROVED it was reachable — a
56682
57160
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56728,7 +57206,6 @@ var BATTERY_DEVICE_TYPES = [
56728
57206
  /** Generic Customize target for the percentage reading (D936). */
56729
57207
  var batteryCustomize = {
56730
57208
  version: 1,
56731
- advanced: "derive",
56732
57209
  targets: [{
56733
57210
  id: "percentage",
56734
57211
  cardinality: "scalar",
@@ -56737,7 +57214,7 @@ var batteryCustomize = {
56737
57214
  stateField: "percentage"
56738
57215
  },
56739
57216
  deviceTypes: BATTERY_DEVICE_TYPES,
56740
- apply: "add-or-replace",
57217
+ apply: "add",
56741
57218
  path: "percentage",
56742
57219
  sources: [{
56743
57220
  kind: "field",
@@ -56858,10 +57335,16 @@ onStatusChanged: { data: object({
56858
57335
  /**
56859
57336
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56860
57337
  */
56861
- history: { fields: { percentage: {
56862
- kind: "gauge",
56863
- unit: "%"
56864
- } } },
57338
+ history: { fields: {
57339
+ percentage: {
57340
+ kind: "gauge",
57341
+ unit: "%"
57342
+ },
57343
+ volts: {
57344
+ kind: "gauge",
57345
+ unit: "V"
57346
+ }
57347
+ } },
56865
57348
  customize: batteryCustomize,
56866
57349
  /** Clock fields: written, but excluded from the compare that decides
56867
57350
  * whether persisting is worth a SQLite commit. */
@@ -56908,12 +57391,7 @@ var binaryCapability = {
56908
57391
  history: { fields: { on: { kind: "boolean" } } },
56909
57392
  /** Clock fields: written, but excluded from the compare that decides
56910
57393
  * whether persisting is worth a SQLite commit. */
56911
- volatileStateFields: ["lastChangedAt"],
56912
- customize: {
56913
- version: 1,
56914
- advanced: "derive",
56915
- targets: []
56916
- }
57394
+ volatileStateFields: ["lastChangedAt"]
56917
57395
  };
56918
57396
  /**
56919
57397
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -56929,7 +57407,7 @@ var binaryCapability = {
56929
57407
  */
56930
57408
  var BrightnessStatusSchema = object({
56931
57409
  /** Current level as 0..100 inclusive. Firmware-reported. */
56932
- percentage: number().min(0).max(100),
57410
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56933
57411
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56934
57412
  lastChangedAt: number()
56935
57413
  });
@@ -57383,21 +57861,21 @@ var ClimateControlStatusSchema = object({
57383
57861
  /** Single setpoint in Celsius. Used by single-target modes
57384
57862
  * (heat / cool / dry). Null when the device is in a range mode
57385
57863
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57386
- target: number().nullable(),
57864
+ target: number().nullable().meta(readingMeta("temperature")),
57387
57865
  /** Upper bound of the dual setpoint in Celsius. Populated only
57388
57866
  * in `heat_cool` mode. */
57389
- targetHigh: number().nullable(),
57867
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57390
57868
  /** Lower bound of the dual setpoint in Celsius. Populated only
57391
57869
  * in `heat_cool` mode. */
57392
- targetLow: number().nullable(),
57870
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57393
57871
  /** Target relative humidity (0..100). Null when the device has no
57394
57872
  * humidity control. */
57395
- targetHumidity: number().min(0).max(100).nullable(),
57873
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57396
57874
  /** Current measured temperature in Celsius. Read-only — pushed by
57397
57875
  * the device's internal sensor. */
57398
- currentTemp: number().nullable(),
57876
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57399
57877
  /** Current measured relative humidity (0..100). Read-only. */
57400
- currentHumidity: number().min(0).max(100).nullable(),
57878
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57401
57879
  /** Vertical louver swing on/off. Null when the device has no vertical
57402
57880
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57403
57881
  swingVertical: boolean().nullable(),
@@ -57405,11 +57883,11 @@ var ClimateControlStatusSchema = object({
57405
57883
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57406
57884
  swingHorizontal: boolean().nullable(),
57407
57885
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57408
- minTemp: number().nullable().optional(),
57886
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57409
57887
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57410
- maxTemp: number().nullable().optional(),
57888
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57411
57889
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57412
- tempStep: number().positive().nullable().optional(),
57890
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57413
57891
  /** Ms epoch when the slice was last updated (push or command). */
57414
57892
  lastFetchedAt: number()
57415
57893
  });
@@ -57551,17 +58029,17 @@ var climateControlCapability = {
57551
58029
  * modes to expose in the picker without a slice fetch.
57552
58030
  */
57553
58031
  var RgbTripletSchema = object({
57554
- r: number().int().min(0).max(255),
57555
- g: number().int().min(0).max(255),
57556
- b: number().int().min(0).max(255)
58032
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58033
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58034
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57557
58035
  });
57558
58036
  var HsvTripletSchema = object({
57559
58037
  /** Hue in degrees, 0..360. */
57560
- h: number().min(0).max(360),
58038
+ h: number().min(0).max(360).meta(readingMeta("none")),
57561
58039
  /** Saturation as 0..100 inclusive. */
57562
- s: number().min(0).max(100),
58040
+ s: number().min(0).max(100).meta(readingMeta("none")),
57563
58041
  /** Value/brightness as 0..100 inclusive. */
57564
- v: number().min(0).max(100)
58042
+ v: number().min(0).max(100).meta(readingMeta("none"))
57565
58043
  });
57566
58044
  var ColorInputSchema = discriminatedUnion("mode", [
57567
58045
  object({
@@ -57590,7 +58068,7 @@ var ColorStatusSchema = object({
57590
58068
  /** Populated when `mode === 'hsv'`. */
57591
58069
  hsv: HsvTripletSchema.optional(),
57592
58070
  /** Populated when `mode === 'mired'`. */
57593
- mireds: number().int().optional(),
58071
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57594
58072
  /** Ms epoch of the last operator-driven change. */
57595
58073
  lastChangedAt: number()
57596
58074
  });
@@ -57793,18 +58271,12 @@ _enum([
57793
58271
  "empty",
57794
58272
  "full"
57795
58273
  ]);
57796
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57797
- * each. */
57798
- var ConsumableRemainingUnitSchema = _enum([
57799
- "days",
57800
- "hours",
57801
- "cycles"
57802
- ]);
57803
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57804
- * days). `value` null = unknown (D393). */
58274
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58275
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58276
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57805
58277
  var ConsumableRemainingSchema = object({
57806
- value: number().min(0).nullable(),
57807
- unit: ConsumableRemainingUnitSchema
58278
+ value: number().min(0).nullable().meta(readingMeta("device")),
58279
+ kind: ConsumableRemainingKindSchema
57808
58280
  });
57809
58281
  /** A single consumable item. Either a continuous `level` (remaining
57810
58282
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57817,7 +58289,7 @@ var ConsumableItemSchema = object({
57817
58289
  /** Display name. */
57818
58290
  label: string().min(1),
57819
58291
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57820
- level: number().min(0).max(100).nullable(),
58292
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57821
58293
  /** The provider's discrete verdict when known (binary mode). */
57822
58294
  status: ConsumableStatusSchema.nullable(),
57823
58295
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57899,8 +58371,8 @@ var consumablesCustomize = {
57899
58371
  transforms: [{ kind: "identity" }]
57900
58372
  }],
57901
58373
  literals: [{
57902
- path: "remaining.unit",
57903
- values: [...ConsumableRemainingUnitSchema.options]
58374
+ path: "remaining.kind",
58375
+ values: [...ConsumableRemainingKindSchema.options]
57904
58376
  }]
57905
58377
  },
57906
58378
  {
@@ -58029,7 +58501,6 @@ reset: method(object({
58029
58501
  },
58030
58502
  "remaining.value": {
58031
58503
  kind: "gauge",
58032
- unitFrom: "remaining.unit",
58033
58504
  deadband: 1,
58034
58505
  displayName: "Remaining"
58035
58506
  }
@@ -58099,10 +58570,9 @@ var contactCapability = {
58099
58570
  * (empty array for the other kinds). It lives in the slice because
58100
58571
  * HA `select.*` entities may update their options at runtime.
58101
58572
  *
58102
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58103
- * `kind === 'numeric'` and are absent (undefined) for all other
58104
- * kinds. They are populated live from HA attributes on each state
58105
- * push — consistent with `options` and the sensor-unit approach.
58573
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58574
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58575
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58106
58576
  */
58107
58577
  var ControlStatusSchema = object({
58108
58578
  kind: _enum([
@@ -58114,39 +58584,17 @@ var ControlStatusSchema = object({
58114
58584
  /** Current value. Numeric kind → number; select/text/datetime → string.
58115
58585
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58116
58586
  * or time-only — HA accepts all three forms). */
58117
- value: union([number(), string()]),
58587
+ value: union([number().meta(readingMeta("device")), string()]),
58118
58588
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58119
58589
  options: array(string()),
58120
58590
  /** Ms epoch when the slice was last updated. */
58121
58591
  lastChangedAt: number(),
58122
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58123
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58124
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58125
- unit: string().optional(),
58126
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58127
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58128
- min: number().optional(),
58129
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58130
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58131
- max: number().optional(),
58132
- /** Step increment. Populated live from HA `attributes.step`.
58133
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58134
- step: number().optional(),
58135
- /** Suggested decimal places for numeric display. Populated live from HA
58136
- * `attributes.suggested_display_precision`, or derived from `step` when the
58137
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58138
- * other kinds. Falls back to auto-formatting when absent. */
58139
- precision: number().int().min(0).max(10).optional(),
58140
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58141
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58142
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58143
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58144
- * `has_date`/`has_time` attributes on each push. */
58145
- format: _enum([
58146
- "date",
58147
- "time",
58148
- "datetime"
58149
- ]).optional()
58592
+ /** Minimum allowed value, in the device quantity's base unit. */
58593
+ min: number().optional().meta(readingMeta("device")),
58594
+ /** Maximum allowed value, in the device quantity's base unit. */
58595
+ max: number().optional().meta(readingMeta("device")),
58596
+ /** Step increment, in the device quantity's base unit. */
58597
+ step: number().optional().meta(readingMeta("device"))
58150
58598
  });
58151
58599
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58152
58600
  object({
@@ -58212,9 +58660,9 @@ var CoverStatusSchema = object({
58212
58660
  ]),
58213
58661
  /** 0 = fully closed, 100 = fully open. Null when the device has
58214
58662
  * no intermediate position surface. */
58215
- position: number().min(0).max(100).nullable(),
58663
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58216
58664
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58217
- tiltPosition: number().min(0).max(100).nullable(),
58665
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58218
58666
  /** Ms epoch when the slice was last updated. */
58219
58667
  lastChangedAt: number()
58220
58668
  });
@@ -58317,9 +58765,9 @@ var NormalizedRangeSchema$1 = object({
58317
58765
  var DayNightStatusSchema = object({
58318
58766
  mode: DayNightModeSchema,
58319
58767
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58320
- sensitivity: number().optional(),
58768
+ sensitivity: number().optional().meta(readingMeta("none")),
58321
58769
  /** Delay before the IR-cut filter flips, in seconds. */
58322
- switchDelaySec: number().optional(),
58770
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58323
58771
  lastFetchedAt: number()
58324
58772
  });
58325
58773
  /**
@@ -58537,11 +58985,11 @@ var DishwasherStatusSchema = object({
58537
58985
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58538
58986
  availablePrograms: array(string()),
58539
58987
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58540
- remainingSeconds: number().int().nonnegative().nullable(),
58988
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58541
58989
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58542
- delayedStartSeconds: number().int().nonnegative().nullable(),
58990
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58543
58991
  /** Cycle completion 0..100, or null when not applicable. */
58544
- progressPercent: number().min(0).max(100).nullable(),
58992
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58545
58993
  /** True when the door is reported open; null when the door state is not known. */
58546
58994
  doorOpen: boolean().nullable(),
58547
58995
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58565,7 +59013,7 @@ var DishwasherStatusSchema = object({
58565
59013
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58566
59014
  machineCareDue: boolean().nullable(),
58567
59015
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58568
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59016
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58569
59017
  /** Option states: null when the appliance does not expose the option or has not said. */
58570
59018
  halfLoad: boolean().nullable(),
58571
59019
  hygienePlus: boolean().nullable(),
@@ -58580,7 +59028,7 @@ var DishwasherStatusSchema = object({
58580
59028
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58581
59029
  activeEvents: array(string()),
58582
59030
  /** Numeric error code when the appliance reports one; null otherwise. */
58583
- errorCode: number().nullable(),
59031
+ errorCode: number().nullable().meta(readingMeta("none")),
58584
59032
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58585
59033
  errorLabel: string().nullable(),
58586
59034
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58811,7 +59259,7 @@ var DoorbellStatusSchema = object({
58811
59259
  /** Ms epoch of the last press. null = never observed since this provider started. */
58812
59260
  lastPressedAt: number().nullable(),
58813
59261
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58814
- pressCountSinceStart: number()
59262
+ pressCountSinceStart: number().meta(readingMeta("count"))
58815
59263
  });
58816
59264
  var DoorbellPressEventSchema = object({
58817
59265
  deviceId: number(),
@@ -58876,17 +59324,7 @@ onPressed: {
58876
59324
  history: "none",
58877
59325
  customize: doorbellCustomize
58878
59326
  };
58879
- /**
58880
- * Enum-state sensor — a string value picked from a finite option set.
58881
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58882
- * states, weather conditions, contact-source channels, mode strings).
58883
- * The option set is stable across observations; it is persisted in the
58884
- * device config blob (via the control slice at adoption) and is NOT
58885
- * stored in `sourceInfo`.
58886
- */
58887
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58888
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58889
- var EnumSensorDateTimeFormatSchema = _enum([
59327
+ _enum([
58890
59328
  "date",
58891
59329
  "time",
58892
59330
  "datetime"
@@ -58894,12 +59332,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58894
59332
  var EnumSensorStatusSchema = object({
58895
59333
  value: string(),
58896
59334
  /**
58897
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58898
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58899
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58900
- * a time-only sensor → `'time'`.
59335
+ * Set when `value` is a timestamp rather than an enum state, so history
59336
+ * does not learn ISO strings as states. The display shape (date / time /
59337
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58901
59338
  */
58902
- format: EnumSensorDateTimeFormatSchema.optional(),
59339
+ timestamp: boolean().optional(),
58903
59340
  /** Ms epoch when the slice was last updated. */
58904
59341
  lastFetchedAt: number()
58905
59342
  });
@@ -58926,11 +59363,11 @@ var enumSensorCapability = {
58926
59363
  * History (D850): the value as an `enum` series (an appliance's program or
58927
59364
  * operation state, an HVAC action). The option set is per device, so the
58928
59365
  * value list is LEARNED as values are first seen (bounded). A sensor with a
58929
- * `format` carries a timestamp, not a state, and is not recorded.
59366
+ * `timestamp` carries a clock, not a state, and is not recorded.
58930
59367
  */
58931
59368
  history: { fields: { value: {
58932
59369
  kind: "enum",
58933
- skipWhenSet: "format"
59370
+ skipWhenSet: "timestamp"
58934
59371
  } } },
58935
59372
  /** Clock fields: written, but excluded from the compare that decides
58936
59373
  * whether persisting is worth a SQLite commit. */
@@ -58950,13 +59387,13 @@ var EventFireSchema = object({
58950
59387
  deviceId: number(),
58951
59388
  eventType: string(),
58952
59389
  data: record(string(), unknown()).nullable(),
58953
- timestamp: number(),
58954
- seq: number()
59390
+ timestamp: number().meta(readingMeta("none")),
59391
+ seq: number().meta(readingMeta("count"))
58955
59392
  });
58956
59393
  var EventEmitterStatusSchema = object({
58957
59394
  eventTypes: array(string()),
58958
59395
  lastEvent: EventFireSchema.nullable(),
58959
- eventCountSinceStart: number()
59396
+ eventCountSinceStart: number().meta(readingMeta("count"))
58960
59397
  });
58961
59398
  var eventEmitterCapability = {
58962
59399
  name: "event-emitter",
@@ -59369,10 +59806,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59369
59806
  var FanControlStatusSchema = object({
59370
59807
  /** Active speed as 0..100 inclusive. Null when the device has no
59371
59808
  * speed surface (single-speed fan). */
59372
- percentage: number().min(0).max(100).nullable(),
59809
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59373
59810
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59374
59811
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59375
- percentageStep: number().positive().optional(),
59812
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59376
59813
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59377
59814
  * — empty string when no preset is active or supported. */
59378
59815
  preset: string(),
@@ -59493,7 +59930,7 @@ var FeatureProbeStatusSchema = object({
59493
59930
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59494
59931
  model: string().nullable(),
59495
59932
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59496
- channelCount: number().nullable(),
59933
+ channelCount: number().nullable().meta(readingMeta("count")),
59497
59934
  /**
59498
59935
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59499
59936
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59616,9 +60053,91 @@ var gasCapability = {
59616
60053
  * whether persisting is worth a SQLite commit. */
59617
60054
  volatileStateFields: ["lastChangedAt"]
59618
60055
  };
60056
+ var GroupCommandsSchema = object({
60057
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60058
+ results: array(object({
60059
+ deviceId: number().int().nonnegative(),
60060
+ cap: string().min(1),
60061
+ method: string().min(1),
60062
+ ok: boolean(),
60063
+ error: string().optional(),
60064
+ /** Ms epoch of the attempt. */
60065
+ at: number()
60066
+ })) });
60067
+ var groupCommandsCapability = {
60068
+ name: "group-commands",
60069
+ scope: "device",
60070
+ deviceNative: true,
60071
+ mode: "singleton",
60072
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60073
+ methods: {},
60074
+ status: {
60075
+ schema: GroupCommandsSchema,
60076
+ kind: "push"
60077
+ },
60078
+ runtimeState: GroupCommandsSchema,
60079
+ /**
60080
+ * Runtime-state durability: **session** — the last attempt. A restored
60081
+ * failure would outlive the command that caused it.
60082
+ *
60083
+ * See `RuntimeStateDurability`. Enforced by
60084
+ * `scripts/check-runtime-state-durability.ts`.
60085
+ */
60086
+ durability: "session",
60087
+ history: "none"
60088
+ };
60089
+ /**
60090
+ * How many members of a helper group answered (D944). Counts are integers,
60091
+ * not measurements: zero included is a real count, never a stand-in reading.
60092
+ * The reading itself stays on the output capability. History is none — the
60093
+ * series that later statistics read is the output cap's own history block.
60094
+ */
60095
+ var GroupStatusSchema = object({
60096
+ /** Members whose current value entered the aggregate. */
60097
+ included: number().int().nonnegative().meta(readingMeta("count", {
60098
+ precision: 0,
60099
+ label: "Reporting"
60100
+ })),
60101
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60102
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60103
+ precision: 0,
60104
+ label: "Excluded"
60105
+ })),
60106
+ /** `included` + `excluded`. */
60107
+ total: number().int().nonnegative().meta(readingMeta("count", {
60108
+ precision: 0,
60109
+ label: "Members"
60110
+ }))
60111
+ });
60112
+ var groupStatusCapability = {
60113
+ name: "group-status",
60114
+ scope: "device",
60115
+ deviceNative: true,
60116
+ mode: "singleton",
60117
+ deviceTypes: [
60118
+ DeviceType.Sensor,
60119
+ DeviceType.Switch,
60120
+ DeviceType.Light
60121
+ ],
60122
+ methods: {},
60123
+ status: {
60124
+ schema: GroupStatusSchema,
60125
+ kind: "push"
60126
+ },
60127
+ runtimeState: GroupStatusSchema,
60128
+ /**
60129
+ * Runtime-state durability: **session** — recomputed from members on the
60130
+ * next source event; a restored count would outlive the readings it named.
60131
+ *
60132
+ * See `RuntimeStateDurability`. Enforced by
60133
+ * `scripts/check-runtime-state-durability.ts`.
60134
+ */
60135
+ durability: "session",
60136
+ history: "none"
60137
+ };
59619
60138
  var HeatingValveStatusSchema = object({
59620
60139
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59621
- position: number().min(0).max(100).nullable(),
60140
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59622
60141
  /** Ms epoch when the slice was last updated (push or poll). */
59623
60142
  lastFetchedAt: number()
59624
60143
  });
@@ -59661,9 +60180,9 @@ var HumidifierStatusSchema = object({
59661
60180
  /** Whether the humidifier is currently on. */
59662
60181
  on: boolean(),
59663
60182
  /** Current measured relative humidity (0..100). Null when not reported. */
59664
- currentHumidity: number().min(0).max(100).nullable(),
60183
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59665
60184
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59666
- targetHumidity: number().min(0).max(100).nullable(),
60185
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59667
60186
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59668
60187
  * device has no mode surface. */
59669
60188
  mode: string().nullable(),
@@ -59673,9 +60192,9 @@ var HumidifierStatusSchema = object({
59673
60192
  * `idle` / `off`). Null when the device doesn't report it. */
59674
60193
  action: string().nullable(),
59675
60194
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59676
- minHumidity: number().nullable(),
60195
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59677
60196
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59678
- maxHumidity: number().nullable(),
60197
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59679
60198
  /** Ms epoch when the slice was last updated. */
59680
60199
  lastChangedAt: number()
59681
60200
  });
@@ -59745,22 +60264,13 @@ var humidifierCapability = {
59745
60264
  * `sourceInfo`.
59746
60265
  */
59747
60266
  var HumiditySensorStatusSchema = object({
59748
- /** Current relative humidity, 0..100. */
59749
- percent: number().min(0).max(100).meta({
59750
- unit: "%",
60267
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60268
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60269
+ precision: 0,
59751
60270
  label: "Humidity"
59752
- }),
60271
+ })),
59753
60272
  /** Ms epoch when the slice was last updated. */
59754
- lastFetchedAt: number(),
59755
- /** Live display unit from the upstream source (e.g. HA
59756
- * `attributes.unit_of_measurement`). The UI prefers this over the
59757
- * role's canonical unit. Absent → fall back to the canonical unit. */
59758
- unit: string().optional(),
59759
- /** Suggested decimal places for numeric display.
59760
- * Populated live from the upstream source when provided (e.g. HA
59761
- * `attributes.suggested_display_precision`). Falls back to
59762
- * auto-formatting when absent. */
59763
- precision: number().int().min(0).max(10).optional()
60273
+ lastFetchedAt: number()
59764
60274
  });
59765
60275
  var humiditySensorCapability = {
59766
60276
  name: "humidity-sensor",
@@ -59791,12 +60301,7 @@ var humiditySensorCapability = {
59791
60301
  } } },
59792
60302
  /** Clock fields: written, but excluded from the compare that decides
59793
60303
  * whether persisting is worth a SQLite commit. */
59794
- volatileStateFields: ["lastFetchedAt"],
59795
- customize: {
59796
- version: 1,
59797
- advanced: "derive",
59798
- targets: []
59799
- }
60304
+ volatileStateFields: ["lastFetchedAt"]
59800
60305
  };
59801
60306
  /**
59802
60307
  * Image display cap. Models a single still image exposed by an integration —
@@ -59817,7 +60322,7 @@ var ImageStatusSchema = object({
59817
60322
  * entity exposes no `entity_picture` (yet). */
59818
60323
  url: string().nullable(),
59819
60324
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59820
- lastUpdated: number().nullable()
60325
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59821
60326
  });
59822
60327
  var imageCapability = {
59823
60328
  name: "image",
@@ -59905,19 +60410,19 @@ var NormalizedRangeSchema = object({
59905
60410
  */
59906
60411
  var ImageSettingsStatusSchema = object({
59907
60412
  /** Normalized 0–100. */
59908
- brightness: number().optional(),
60413
+ brightness: number().optional().meta(readingMeta("none")),
59909
60414
  /** Normalized 0–100. */
59910
- contrast: number().optional(),
60415
+ contrast: number().optional().meta(readingMeta("none")),
59911
60416
  /** Normalized 0–100. */
59912
- saturation: number().optional(),
60417
+ saturation: number().optional().meta(readingMeta("none")),
59913
60418
  /** Normalized 0–100. */
59914
- sharpness: number().optional(),
60419
+ sharpness: number().optional().meta(readingMeta("none")),
59915
60420
  mirror: boolean().optional(),
59916
60421
  flip: boolean().optional(),
59917
60422
  rotate: ImageRotateSchema.optional(),
59918
60423
  whiteBalance: WhiteBalanceModeSchema.optional(),
59919
60424
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
59920
- warmth: number().optional(),
60425
+ warmth: number().optional().meta(readingMeta("none")),
59921
60426
  exposureMode: ExposureModeSchema.optional(),
59922
60427
  backlightMode: BacklightModeSchema.optional(),
59923
60428
  lastFetchedAt: number()
@@ -60196,10 +60701,10 @@ var IntercomAbilitySchema = object({
60196
60701
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60197
60702
  codecs: array(string()),
60198
60703
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60199
- sampleRate: number(),
60704
+ sampleRate: number().meta(readingMeta("none")),
60200
60705
  duplex: _enum(["half", "full"]),
60201
60706
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60202
- maxBacklogMs: number()
60707
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60203
60708
  });
60204
60709
  var IntercomStatusSchema = object({
60205
60710
  /** True while a talk session is open. */
@@ -60426,13 +60931,13 @@ var LawnMowerControlStatusSchema = object({
60426
60931
  activity: LawnMowerActivitySchema,
60427
60932
  /** 0..100 battery percentage. Null when the device has no battery
60428
60933
  * reading. */
60429
- batteryLevel: number().min(0).max(100).nullable(),
60934
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60430
60935
  /** 0..100 mowing-completion percentage of the current task, or null when no
60431
60936
  * task is active / progress is unavailable. */
60432
- progressPercent: number().min(0).max(100).nullable(),
60937
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60433
60938
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60434
60939
  * or null when unknown. */
60435
- currentCode: number().nullable(),
60940
+ currentCode: number().nullable().meta(readingMeta("none")),
60436
60941
  /** Human label for {@link currentCode}, or null when undecodable. */
60437
60942
  currentCodeLabel: string().nullable(),
60438
60943
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -60965,7 +61470,7 @@ var MediaPlayerStatusSchema = object({
60965
61470
  state: MediaPlayerStateSchema,
60966
61471
  /** Volume as 0..100 inclusive. Null when the device has no volume
60967
61472
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
60968
- volumeLevel: number().min(0).max(100).nullable(),
61473
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60969
61474
  /** Mute toggle distinct from volume=0. Null when no mute surface.
60970
61475
  * Pair with `DeviceFeature.MediaPlayerMute`. */
60971
61476
  isMuted: boolean().nullable(),
@@ -60978,10 +61483,10 @@ var MediaPlayerStatusSchema = object({
60978
61483
  currentMedia: MediaInfoSchema.nullable(),
60979
61484
  /** Current playback position in ms. Null when not seekable or
60980
61485
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
60981
- positionMs: number().int().nonnegative().nullable(),
61486
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
60982
61487
  /** Total duration of the current media in ms. Null when unknown
60983
61488
  * (live stream) or nothing is playing. */
60984
- durationMs: number().int().nonnegative().nullable(),
61489
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
60985
61490
  /** Shuffle toggle. Null when no shuffle surface.
60986
61491
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
60987
61492
  shuffle: boolean().nullable(),
@@ -61290,7 +61795,7 @@ var motionCustomize = {
61290
61795
  eventName: "onMotionChanged"
61291
61796
  },
61292
61797
  deviceTypes: [DeviceType.Camera],
61293
- apply: "replace",
61798
+ apply: "add",
61294
61799
  applyAuthority: {
61295
61800
  kind: "camera-sensors",
61296
61801
  timeoutField: "motionCooldownMs"
@@ -61362,7 +61867,7 @@ var MotionStatusSchema = object({
61362
61867
  * it, every provider seeded it at boot, and every session left it
61363
61868
  * `detected: true` after its detach.
61364
61869
  */
61365
- autoClearAfterMs: number().nullable()
61870
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61366
61871
  });
61367
61872
  /**
61368
61873
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61399,12 +61904,18 @@ var motionCapability = {
61399
61904
  methods: {
61400
61905
  /**
61401
61906
  * Pull the current motion state synchronously. Convenience shortcut
61402
- * for consumers that don't need the full status object; equivalent
61403
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61404
- * `getStatus` once the auto-injected status surface lands in every
61405
- * consumer.
61907
+ * for consumers that don't need the full status object. A composed
61908
+ * group answers from its slice (D944): unknown throws, it is not off.
61406
61909
  */
61407
61910
  isDetected: method(object({ deviceId: number() }), boolean()) },
61911
+ /**
61912
+ * A composed motion group serves `isDetected` from `detected` (D944).
61913
+ * Without this the cap stays uncomposable (`has-commands`).
61914
+ */
61915
+ composedMethods: { isDetected: {
61916
+ kind: "slice",
61917
+ field: "detected"
61918
+ } },
61408
61919
  events: {
61409
61920
  /**
61410
61921
  * Fires every time the runner transitions a camera between
@@ -61544,7 +62055,7 @@ var MotionZoneRegionSchema = object({
61544
62055
  * the grid region(s). */
61545
62056
  var MotionZoneStatusSchema = object({
61546
62057
  enabled: boolean(),
61547
- sensitivity: number(),
62058
+ sensitivity: number().meta(readingMeta("none")),
61548
62059
  /** Grid region(s). Today exactly one `grid` shape. */
61549
62060
  regions: array(MotionZoneRegionSchema),
61550
62061
  lastFetchedAt: number()
@@ -61914,7 +62425,7 @@ var NavigationStatusSchema = object({
61914
62425
  following: boolean(),
61915
62426
  flash: boolean(),
61916
62427
  lightMode: NavigationLightModeSchema,
61917
- lightLevel: number().min(40).max(100),
62428
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
61918
62429
  /** Ms epoch when the slice was last updated. */
61919
62430
  lastChangedAt: number()
61920
62431
  });
@@ -62048,13 +62559,13 @@ var NetworkLinkStatusSchema = object({
62048
62559
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62049
62560
  * SKIP a null rather than coerce it.
62050
62561
  */
62051
- signalPercent: number().min(0).max(100).nullable(),
62562
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62052
62563
  /** Raw received signal strength in dBm, when the firmware reports one. */
62053
- rssiDbm: number().optional(),
62564
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62054
62565
  /** Network name of a wireless link, when the firmware reports it. */
62055
62566
  ssid: string().optional(),
62056
62567
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62057
- lastUpdated: number()
62568
+ lastUpdated: number().meta(readingMeta("none"))
62058
62569
  });
62059
62570
  var networkLinkCapability = {
62060
62571
  name: "network-link",
@@ -62439,7 +62950,7 @@ var NotifierStatusSchema = object({
62439
62950
  lastError: string().nullable(),
62440
62951
  /** Number of deliveries currently buffered server-side (rate-limited
62441
62952
  * notifiers may queue). 0 when the provider sends synchronously. */
62442
- queueDepth: number().int().nonnegative(),
62953
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62443
62954
  /** Per-feature capability matrix — authoritative source for the
62444
62955
  * compose-form field gating. */
62445
62956
  supports: NotifierSupportsSchema
@@ -62531,11 +63042,9 @@ onSent: { data: object({
62531
63042
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62532
63043
  * for: water flow, distance, weight, frequency, signal strength, …).
62533
63044
  *
62534
- * The `unit` and `precision` fields carry the live display metadata that
62535
- * the upstream source provides with each state push (HA
62536
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62537
- * The UI reads them directly from the slice — they are the single source
62538
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63045
+ * The quantity is not on this slice. It is chosen when the device is
63046
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63047
+ * that quantity's base unit. `null` is a refused sample (D393).
62539
63048
  *
62540
63049
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62541
63050
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62544,15 +63053,8 @@ onSent: { data: object({
62544
63053
  * numeric values land here.
62545
63054
  */
62546
63055
  var NumericSensorStatusSchema = object({
62547
- value: number(),
62548
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62549
- * Populated live from the upstream source on each state push.
62550
- * Absent for unitless sensors. */
62551
- unit: string().optional(),
62552
- /** Suggested decimal places for numeric display.
62553
- * Populated live from the upstream source when provided.
62554
- * Falls back to auto-formatting when absent. */
62555
- precision: number().int().min(0).max(10).optional(),
63056
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63057
+ value: number().nullable().meta(readingMeta("device")),
62556
63058
  /** Ms epoch when the slice was last updated. */
62557
63059
  lastFetchedAt: number()
62558
63060
  });
@@ -62576,11 +63078,11 @@ var numericSensorCapability = {
62576
63078
  */
62577
63079
  durability: "restored",
62578
63080
  /**
62579
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63081
+ * History (D850): recorded series. The unit is the device quantity's base,
63082
+ * resolved when the series is read — the cap has no single unit.
62580
63083
  */
62581
63084
  history: { fields: { value: {
62582
63085
  kind: "gauge",
62583
- unitFrom: "unit",
62584
63086
  minIntervalMs: 3e4
62585
63087
  } } },
62586
63088
  /** Clock fields: written, but excluded from the compare that decides
@@ -62872,23 +63374,26 @@ var PetFeederStatusSchema = object({
62872
63374
  /** Food currently in the bowl (grams). Null when the device has not
62873
63375
  * reported a reading yet. On dual-hopper models this is the combined
62874
63376
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62875
- foodLevel: number().nullable(),
63377
+ foodLevel: number().nullable().meta(readingMeta("none")),
62876
63378
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62877
63379
  * single-hopper models. */
62878
- food1: number().nullable(),
63380
+ food1: number().nullable().meta(readingMeta("none")),
62879
63381
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62880
63382
  * single-hopper models. */
62881
- food2: number().nullable(),
63383
+ food2: number().nullable().meta(readingMeta("none")),
62882
63384
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62883
63385
  * (`device_class: problem`, on = low). True when the bowl is empty /
62884
63386
  * below the feeder's low threshold. */
62885
63387
  lowFood: boolean(),
62886
63388
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62887
63389
  * device has no battery reading. */
62888
- batteryPower: number().min(0).max(100).nullable(),
62889
- /** Days of desiccant life remaining. Null when the model has no
62890
- * desiccant sensor. */
62891
- desiccantLeftDays: number().nullable(),
63390
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63391
+ /**
63392
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63393
+ * Providers convert from the days the feeder reports. Null when the
63394
+ * model has no desiccant sensor.
63395
+ */
63396
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
62892
63397
  /** True while a feed is in progress. */
62893
63398
  feeding: boolean(),
62894
63399
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62899,7 +63404,7 @@ var PetFeederStatusSchema = object({
62899
63404
  * with `errorCode` for consumers that want the raw integer. */
62900
63405
  error: string().nullable(),
62901
63406
  /** Raw device error code (0 / null = no error). */
62902
- errorCode: number().nullable(),
63407
+ errorCode: number().nullable().meta(readingMeta("none")),
62903
63408
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62904
63409
  isDualHopper: boolean(),
62905
63410
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -62909,7 +63414,7 @@ var PetFeederStatusSchema = object({
62909
63414
  /** Play a chime when dispensing. */
62910
63415
  feedSound: boolean(),
62911
63416
  /** Speaker / prompt volume level (device-scaled integer). */
62912
- volume: number(),
63417
+ volume: number().meta(readingMeta("none")),
62913
63418
  /** Ms epoch when the slice was last refreshed from the cloud. */
62914
63419
  lastFetchedAt: number()
62915
63420
  });
@@ -63028,7 +63533,7 @@ var petFeederCapability = {
63028
63533
  },
63029
63534
  desiccantLeftDays: {
63030
63535
  kind: "gauge",
63031
- unit: "days"
63536
+ unit: "s"
63032
63537
  }
63033
63538
  } }
63034
63539
  };
@@ -63429,36 +63934,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63429
63934
  */
63430
63935
  var PowerMeterStatusSchema = object({
63431
63936
  /** Instantaneous power draw in watts. */
63432
- watts: number().optional().meta({
63433
- unit: "W",
63434
- label: "Power"
63435
- }),
63937
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63436
63938
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63437
- kwhTotal: number().optional().meta({
63438
- unit: "kWh",
63439
- label: "Energy"
63440
- }),
63939
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63441
63940
  /** Voltage in volts. */
63442
- volts: number().optional(),
63941
+ volts: number().optional().meta(readingMeta("voltage")),
63443
63942
  /** Current in amperes. */
63444
- amps: number().optional(),
63445
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63446
- va: number().optional(),
63943
+ amps: number().optional().meta(readingMeta("current")),
63944
+ /** Apparent power in volt-amperes. */
63945
+ va: number().optional().meta(readingMeta("apparent-power")),
63447
63946
  /** Power factor (0..1) if reported. */
63448
- powerFactor: number().min(0).max(1).optional(),
63947
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63449
63948
  /** Ms epoch when the slice was last updated. */
63450
- lastFetchedAt: number(),
63451
- /** Live display unit of the single metric this slice carries (e.g. HA
63452
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63453
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63454
- * unit per slice is unambiguous. The UI prefers this over the role's
63455
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63456
- unit: string().optional(),
63457
- /** Suggested decimal places for numeric display.
63458
- * Populated live from the upstream source when provided (e.g. HA
63459
- * `attributes.suggested_display_precision`). Falls back to
63460
- * auto-formatting when absent. */
63461
- precision: number().int().min(0).max(10).optional()
63949
+ lastFetchedAt: number()
63462
63950
  });
63463
63951
  var powerMeterCapability = {
63464
63952
  name: "power-meter",
@@ -63522,12 +64010,7 @@ var powerMeterCapability = {
63522
64010
  } },
63523
64011
  /** Clock fields: written, but excluded from the compare that decides
63524
64012
  * whether persisting is worth a SQLite commit. */
63525
- volatileStateFields: ["lastFetchedAt"],
63526
- customize: {
63527
- version: 1,
63528
- advanced: "derive",
63529
- targets: []
63530
- }
64013
+ volatileStateFields: ["lastFetchedAt"]
63531
64014
  };
63532
64015
  /**
63533
64016
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63547,11 +64030,11 @@ var powerMeterCapability = {
63547
64030
  */
63548
64031
  var GpsLocationSchema = object({
63549
64032
  /** Latitude in decimal degrees, -90..90. */
63550
- latitude: number().min(-90).max(90),
64033
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63551
64034
  /** Longitude in decimal degrees, -180..180. */
63552
- longitude: number().min(-180).max(180),
64035
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63553
64036
  /** Reported accuracy in meters (lower = better). */
63554
- accuracyMeters: number().nonnegative()
64037
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63555
64038
  });
63556
64039
  var PresenceStatusSchema = object({
63557
64040
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63563,7 +64046,7 @@ var PresenceStatusSchema = object({
63563
64046
  gps: GpsLocationSchema.nullable(),
63564
64047
  /** Optional battery level of the tracking device (0..100). Null
63565
64048
  * when not reported. */
63566
- batteryPercent: number().min(0).max(100).nullable(),
64049
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63567
64050
  /** Ms epoch when the slice was last updated. */
63568
64051
  lastChangedAt: number()
63569
64052
  });
@@ -63613,19 +64096,10 @@ var presenceCapability = {
63613
64096
  * `sourceInfo`.
63614
64097
  */
63615
64098
  var PressureSensorStatusSchema = object({
63616
- /** Current pressure in hPa. */
63617
- hpa: number(),
64099
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64100
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63618
64101
  /** Ms epoch when the slice was last updated. */
63619
- lastFetchedAt: number(),
63620
- /** Live display unit from the upstream source (e.g. HA
63621
- * `attributes.unit_of_measurement`). The UI prefers this over the
63622
- * role's canonical unit. Absent → fall back to the canonical unit. */
63623
- unit: string().optional(),
63624
- /** Suggested decimal places for numeric display.
63625
- * Populated live from the upstream source when provided (e.g. HA
63626
- * `attributes.suggested_display_precision`). Falls back to
63627
- * auto-formatting when absent. */
63628
- precision: number().int().min(0).max(10).optional()
64102
+ lastFetchedAt: number()
63629
64103
  });
63630
64104
  var pressureSensorCapability = {
63631
64105
  name: "pressure-sensor",
@@ -64039,8 +64513,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64039
64513
  }), _void(), { kind: "mutation" });
64040
64514
  var PtzAutotrackSettingsSchema = object({
64041
64515
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64042
- stopDelaySeconds: number().int().min(0).max(300),
64043
- disappearDelaySeconds: number().int().min(0).max(300)
64516
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64517
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64044
64518
  });
64045
64519
  /**
64046
64520
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64202,7 +64676,7 @@ var RunningStateStatusSchema = object({
64202
64676
  /** null = unknown — never coerced to false (D393). */
64203
64677
  running: boolean().nullable(),
64204
64678
  /** Epoch ms the current boolean began; null while `running` is null. */
64205
- since: number().nullable(),
64679
+ since: number().nullable().meta(readingMeta("none")),
64206
64680
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64207
64681
  phase: string().nullable(),
64208
64682
  /** null until a reading has been published (D393). */
@@ -64217,7 +64691,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64217
64691
  /** Generic Customize target for the running boolean (D936). */
64218
64692
  var runningCustomize = {
64219
64693
  version: 1,
64220
- advanced: "derive",
64221
64694
  targets: [{
64222
64695
  id: "running",
64223
64696
  cardinality: "scalar",
@@ -64237,7 +64710,7 @@ var runningCustomize = {
64237
64710
  DeviceType.Thermostat,
64238
64711
  DeviceType.PetFeeder
64239
64712
  ],
64240
- apply: "add-or-replace",
64713
+ apply: "add",
64241
64714
  path: "running",
64242
64715
  sources: [
64243
64716
  {
@@ -65501,9 +65974,9 @@ var RecordTriggerSchema = _enum([
65501
65974
  */
65502
65975
  var RecordWindowSchema = object({
65503
65976
  trigger: RecordTriggerSchema,
65504
- day: number().int().min(0).max(6),
65505
- startMinute: number().int().min(0).max(1439),
65506
- endMinute: number().int().min(1).max(1440)
65977
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65978
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65979
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65507
65980
  });
65508
65981
  /** Status of one physical volume, as the camera itself describes it. */
65509
65982
  var OnboardStorageVolumeSchema = object({
@@ -65579,10 +66052,10 @@ var RecordingOnboardStatusSchema = object({
65579
66052
  /** Master "record to the card at all" switch. */
65580
66053
  enabled: boolean().nullable(),
65581
66054
  overwriteWhenFull: boolean().nullable(),
65582
- preRecordSec: number().nullable(),
65583
- postRecordSec: number().nullable(),
66055
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66056
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65584
66057
  /** Length of one recorded file, in minutes. */
65585
- segmentMinutes: number().nullable(),
66058
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65586
66059
  /** The primary track's weekly windows, flattened. */
65587
66060
  windows: array(RecordWindowSchema),
65588
66061
  /**
@@ -65598,7 +66071,7 @@ var RecordingOnboardStatusSchema = object({
65598
66071
  * provider reporting a non-zero count also reports the schedule as not
65599
66072
  * writable.
65600
66073
  */
65601
- unreadableWindows: number(),
66074
+ unreadableWindows: number().meta(readingMeta("count")),
65602
66075
  /**
65603
66076
  * The camera is scheduled to record and has NO usable storage.
65604
66077
  *
@@ -66443,7 +66916,7 @@ var CamStreamDescriptorSchema = object({
66443
66916
  url: string().optional(),
66444
66917
  codec: string().optional(),
66445
66918
  resolution: CamStreamResolutionSchema.optional(),
66446
- fps: number().positive().optional(),
66919
+ fps: number().positive().optional().meta(readingMeta("rate")),
66447
66920
  label: string().optional(),
66448
66921
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66449
66922
  deviceFeatures: array(string()).optional(),
@@ -66530,18 +67003,18 @@ var StreamProfileSchema = _enum([
66530
67003
  ]);
66531
67004
  /** The current encoding config of one stream profile. */
66532
67005
  var StreamProfileConfigSchema = object({
66533
- width: number(),
66534
- height: number(),
67006
+ width: number().meta(readingMeta("none")),
67007
+ height: number().meta(readingMeta("none")),
66535
67008
  codec: _enum(["h264", "h265"]),
66536
- framerate: number(),
66537
- bitrate: number(),
67009
+ framerate: number().meta(readingMeta("rate")),
67010
+ bitrate: number().meta(readingMeta("bitrate")),
66538
67011
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66539
67012
  encoderProfile: _enum([
66540
67013
  "high",
66541
67014
  "main",
66542
67015
  "baseline"
66543
67016
  ]).optional(),
66544
- gop: number().optional(),
67017
+ gop: number().optional().meta(readingMeta("none")),
66545
67018
  /**
66546
67019
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66547
67020
  *
@@ -66762,12 +67235,7 @@ var switchCapability = {
66762
67235
  label: "On",
66763
67236
  kind: "boolean"
66764
67237
  }
66765
- }] },
66766
- customize: {
66767
- version: 1,
66768
- advanced: "derive",
66769
- targets: []
66770
- }
67238
+ }] }
66771
67239
  };
66772
67240
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66773
67241
  function hasControlCharacter(value) {
@@ -67350,35 +67818,21 @@ var tamperCapability = {
67350
67818
  * entries with `device_class: temperature` and any future native
67351
67819
  * thermometer.
67352
67820
  *
67353
- * Unit normalisation: providers convert to Celsius before storing.
67354
- * The slice value is always Celsius so cross-cap aggregators
67355
- * (climate-control's `currentTemp`, energy analytics) can compose
67356
- * without per-source unit fixups. The canonical display unit (`°C`) is
67357
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67358
- * `sourceInfo`.
67821
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67822
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67359
67823
  *
67360
67824
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67361
67825
  * frozen feed (provider hung) distinct from a "temperature hasn't
67362
67826
  * changed" steady state.
67363
67827
  */
67364
67828
  var TemperatureSensorStatusSchema = object({
67365
- /** Current temperature in Celsius. */
67366
- celsius: number().meta({
67367
- unit: "°C",
67829
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67830
+ celsius: number().nullable().meta(readingMeta("temperature", {
67831
+ precision: 1,
67368
67832
  label: "Temperature"
67369
- }),
67833
+ })),
67370
67834
  /** Ms epoch when the slice was last updated (push or poll). */
67371
- lastFetchedAt: number(),
67372
- /** Live display unit from the upstream source (e.g. HA
67373
- * `attributes.unit_of_measurement`). The UI prefers this over the
67374
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67375
- * Absent → the UI falls back to the role's canonical unit. */
67376
- unit: string().optional(),
67377
- /** Suggested decimal places for numeric display.
67378
- * Populated live from the upstream source when provided (e.g. HA
67379
- * `attributes.suggested_display_precision`). Falls back to
67380
- * auto-formatting when absent. */
67381
- precision: number().int().min(0).max(10).optional()
67835
+ lastFetchedAt: number()
67382
67836
  });
67383
67837
  var temperatureSensorCapability = {
67384
67838
  name: "temperature-sensor",
@@ -67409,12 +67863,7 @@ var temperatureSensorCapability = {
67409
67863
  } } },
67410
67864
  /** Clock fields: written, but excluded from the compare that decides
67411
67865
  * whether persisting is worth a SQLite commit. */
67412
- volatileStateFields: ["lastFetchedAt"],
67413
- customize: {
67414
- version: 1,
67415
- advanced: "derive",
67416
- targets: []
67417
- }
67866
+ volatileStateFields: ["lastFetchedAt"]
67418
67867
  };
67419
67868
  /**
67420
67869
  * toast — system-scoped singleton capability that streams toast
@@ -67697,16 +68146,16 @@ var VacuumControlStatusSchema = object({
67697
68146
  ]),
67698
68147
  /** 0..100 battery percentage. Null when the device has no battery
67699
68148
  * reading. */
67700
- batteryLevel: number().min(0).max(100).nullable(),
68149
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67701
68150
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67702
68151
  * the vacuum has no speed control. */
67703
68152
  fanSpeed: string().nullable(),
67704
68153
  /** Speed tokens the hardware accepts — drives the UI selector. */
67705
68154
  availableFanSpeeds: array(string()),
67706
68155
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67707
- progressPercent: number().min(0).max(100).nullable(),
68156
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67708
68157
  /** Current error code (0 / null = no error). */
67709
- errorCode: number().nullable(),
68158
+ errorCode: number().nullable().meta(readingMeta("none")),
67710
68159
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67711
68160
  errorLabel: string().nullable(),
67712
68161
  /** Ms epoch when the slice was last updated. */
@@ -67784,7 +68233,7 @@ var ValveStatusSchema = object({
67784
68233
  ]),
67785
68234
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67786
68235
  * intermediate position surface. */
67787
- position: number().min(0).max(100).nullable(),
68236
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67788
68237
  /** Ms epoch when the slice was last updated. */
67789
68238
  lastChangedAt: number()
67790
68239
  });
@@ -67892,9 +68341,9 @@ var vibrationCapability = {
67892
68341
  */
67893
68342
  var WaterHeaterStatusSchema = object({
67894
68343
  /** Current measured temperature. Null when not reported. */
67895
- currentTemp: number().nullable(),
68344
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67896
68345
  /** Target temperature setpoint. Null when no setpoint surface. */
67897
- targetTemp: number().nullable(),
68346
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67898
68347
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67899
68348
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67900
68349
  * device reports an unknown state. */
@@ -67905,9 +68354,9 @@ var WaterHeaterStatusSchema = object({
67905
68354
  * has no away surface. */
67906
68355
  away: boolean().nullable(),
67907
68356
  /** HA `min_temp` attribute. Null when not reported. */
67908
- minTemp: number().nullable(),
68357
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67909
68358
  /** HA `max_temp` attribute. Null when not reported. */
67910
- maxTemp: number().nullable(),
68359
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67911
68360
  /** Ms epoch when the slice was last updated. */
67912
68361
  lastChangedAt: number()
67913
68362
  });
@@ -67992,25 +68441,18 @@ var waterHeaterCapability = {
67992
68441
  * Forecast deferred — current conditions only for v1.
67993
68442
  */
67994
68443
  var WeatherStatusSchema = object({
67995
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
67996
- * when no condition has been reported yet. */
68444
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
67997
68445
  condition: string().nullable(),
67998
- /** Current temperature in the reported unit. Null when not provided. */
67999
- temperature: number().nullable(),
68000
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68001
- temperatureUnit: string().nullable(),
68446
+ /** Current temperature in Celsius. Null when not provided or refused. */
68447
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68002
68448
  /** Relative humidity (0..100). Null when not provided. */
68003
- humidity: number().min(0).max(100).nullable(),
68004
- /** Barometric pressure in the reported unit. Null when not provided. */
68005
- pressure: number().nullable(),
68006
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68007
- pressureUnit: string().nullable(),
68008
- /** Wind speed in the reported unit. Null when not provided. */
68009
- windSpeed: number().nullable(),
68010
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68011
- windSpeedUnit: string().nullable(),
68449
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68450
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68451
+ pressure: number().nullable().meta(readingMeta("pressure")),
68452
+ /** Wind speed in metres per second. Null when not provided or refused. */
68453
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68012
68454
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68013
- windBearing: number().nullable(),
68455
+ windBearing: number().nullable().meta(readingMeta("angle")),
68014
68456
  /** Ms epoch when the slice was last updated. */
68015
68457
  lastFetchedAt: number()
68016
68458
  });
@@ -68038,12 +68480,12 @@ var weatherCapability = {
68038
68480
  */
68039
68481
  durability: "session",
68040
68482
  /**
68041
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68483
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68042
68484
  */
68043
68485
  history: { fields: {
68044
68486
  temperature: {
68045
68487
  kind: "gauge",
68046
- unitFrom: "temperatureUnit",
68488
+ unit: "°C",
68047
68489
  deadband: .2
68048
68490
  },
68049
68491
  humidity: {
@@ -68053,12 +68495,12 @@ var weatherCapability = {
68053
68495
  },
68054
68496
  pressure: {
68055
68497
  kind: "gauge",
68056
- unitFrom: "pressureUnit",
68498
+ unit: "hPa",
68057
68499
  deadband: .5
68058
68500
  },
68059
68501
  windSpeed: {
68060
68502
  kind: "gauge",
68061
- unitFrom: "windSpeedUnit",
68503
+ unit: "m/s",
68062
68504
  deadband: .5
68063
68505
  }
68064
68506
  } }
@@ -68990,6 +69432,389 @@ function runningFromDishwasherOperation(operation, program, previous, fetchedAt,
68990
69432
  if (operation === "Pause") return nativeSlice(false, "Pause", previous, transitionAtMs, fetchedAt, null);
68991
69433
  return nativeSlice(false, operation, previous, transitionAtMs, fetchedAt, null);
68992
69434
  }
69435
+ /**
69436
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
69437
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
69438
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
69439
+ * decimals). K derives from `°C = K − 273.15`.
69440
+ */
69441
+ var UNIT_TABLE = {
69442
+ "°C": {
69443
+ dimension: "temperature",
69444
+ factor: 1,
69445
+ offset: 0
69446
+ },
69447
+ "°F": {
69448
+ dimension: "temperature",
69449
+ factor: 5 / 9,
69450
+ offset: -160 / 9
69451
+ },
69452
+ K: {
69453
+ dimension: "temperature",
69454
+ factor: 1,
69455
+ offset: -273.15
69456
+ },
69457
+ hPa: {
69458
+ dimension: "pressure",
69459
+ factor: 1,
69460
+ offset: 0
69461
+ },
69462
+ kPa: {
69463
+ dimension: "pressure",
69464
+ factor: 10,
69465
+ offset: 0
69466
+ },
69467
+ Pa: {
69468
+ dimension: "pressure",
69469
+ factor: .01,
69470
+ offset: 0
69471
+ },
69472
+ mbar: {
69473
+ dimension: "pressure",
69474
+ factor: 1,
69475
+ offset: 0
69476
+ },
69477
+ bar: {
69478
+ dimension: "pressure",
69479
+ factor: 1e3,
69480
+ offset: 0
69481
+ },
69482
+ inHg: {
69483
+ dimension: "pressure",
69484
+ factor: 33.8639,
69485
+ offset: 0
69486
+ },
69487
+ mmHg: {
69488
+ dimension: "pressure",
69489
+ factor: 1.33322,
69490
+ offset: 0
69491
+ },
69492
+ psi: {
69493
+ dimension: "pressure",
69494
+ factor: 68.9476,
69495
+ offset: 0
69496
+ },
69497
+ "m/s": {
69498
+ dimension: "speed",
69499
+ factor: 1,
69500
+ offset: 0
69501
+ },
69502
+ "km/h": {
69503
+ dimension: "speed",
69504
+ factor: 1 / 3.6,
69505
+ offset: 0
69506
+ },
69507
+ mph: {
69508
+ dimension: "speed",
69509
+ factor: .44704,
69510
+ offset: 0
69511
+ },
69512
+ kn: {
69513
+ dimension: "speed",
69514
+ factor: .514444,
69515
+ offset: 0
69516
+ },
69517
+ "mm/h": {
69518
+ dimension: "precipitation-rate",
69519
+ factor: 1,
69520
+ offset: 0
69521
+ },
69522
+ "in/h": {
69523
+ dimension: "precipitation-rate",
69524
+ factor: 25.4,
69525
+ offset: 0
69526
+ },
69527
+ m: {
69528
+ dimension: "length",
69529
+ factor: 1,
69530
+ offset: 0
69531
+ },
69532
+ mm: {
69533
+ dimension: "length",
69534
+ factor: .001,
69535
+ offset: 0
69536
+ },
69537
+ cm: {
69538
+ dimension: "length",
69539
+ factor: .01,
69540
+ offset: 0
69541
+ },
69542
+ km: {
69543
+ dimension: "length",
69544
+ factor: 1e3,
69545
+ offset: 0
69546
+ },
69547
+ in: {
69548
+ dimension: "length",
69549
+ factor: .0254,
69550
+ offset: 0
69551
+ },
69552
+ ft: {
69553
+ dimension: "length",
69554
+ factor: .3048,
69555
+ offset: 0
69556
+ },
69557
+ mi: {
69558
+ dimension: "length",
69559
+ factor: 1609.344,
69560
+ offset: 0
69561
+ },
69562
+ "m²": {
69563
+ dimension: "area",
69564
+ factor: 1,
69565
+ offset: 0
69566
+ },
69567
+ lx: {
69568
+ dimension: "illuminance",
69569
+ factor: 1,
69570
+ offset: 0
69571
+ },
69572
+ "W/m²": {
69573
+ dimension: "irradiance",
69574
+ factor: 1,
69575
+ offset: 0
69576
+ },
69577
+ W: {
69578
+ dimension: "power",
69579
+ factor: 1,
69580
+ offset: 0
69581
+ },
69582
+ kW: {
69583
+ dimension: "power",
69584
+ factor: 1e3,
69585
+ offset: 0
69586
+ },
69587
+ VA: {
69588
+ dimension: "apparent-power",
69589
+ factor: 1,
69590
+ offset: 0
69591
+ },
69592
+ Wh: {
69593
+ dimension: "energy",
69594
+ factor: 1,
69595
+ offset: 0
69596
+ },
69597
+ kWh: {
69598
+ dimension: "energy",
69599
+ factor: 1e3,
69600
+ offset: 0
69601
+ },
69602
+ MWh: {
69603
+ dimension: "energy",
69604
+ factor: 1e6,
69605
+ offset: 0
69606
+ },
69607
+ V: {
69608
+ dimension: "voltage",
69609
+ factor: 1,
69610
+ offset: 0
69611
+ },
69612
+ mV: {
69613
+ dimension: "voltage",
69614
+ factor: .001,
69615
+ offset: 0
69616
+ },
69617
+ A: {
69618
+ dimension: "current",
69619
+ factor: 1,
69620
+ offset: 0
69621
+ },
69622
+ mA: {
69623
+ dimension: "current",
69624
+ factor: .001,
69625
+ offset: 0
69626
+ },
69627
+ ppm: {
69628
+ dimension: "concentration-volume",
69629
+ factor: 1,
69630
+ offset: 0
69631
+ },
69632
+ ppb: {
69633
+ dimension: "concentration-volume",
69634
+ factor: .001,
69635
+ offset: 0
69636
+ },
69637
+ "µg/m³": {
69638
+ dimension: "concentration-mass",
69639
+ factor: 1,
69640
+ offset: 0
69641
+ },
69642
+ "mg/m³": {
69643
+ dimension: "concentration-mass",
69644
+ factor: 1e3,
69645
+ offset: 0
69646
+ },
69647
+ kg: {
69648
+ dimension: "mass",
69649
+ factor: 1,
69650
+ offset: 0
69651
+ },
69652
+ g: {
69653
+ dimension: "mass",
69654
+ factor: .001,
69655
+ offset: 0
69656
+ },
69657
+ lb: {
69658
+ dimension: "mass",
69659
+ factor: .453592,
69660
+ offset: 0
69661
+ },
69662
+ oz: {
69663
+ dimension: "mass",
69664
+ factor: .0283495,
69665
+ offset: 0
69666
+ },
69667
+ "%": {
69668
+ dimension: "percentage",
69669
+ factor: 1,
69670
+ offset: 0
69671
+ },
69672
+ L: {
69673
+ dimension: "volume",
69674
+ factor: 1,
69675
+ offset: 0
69676
+ },
69677
+ mL: {
69678
+ dimension: "volume",
69679
+ factor: .001,
69680
+ offset: 0
69681
+ },
69682
+ gal: {
69683
+ dimension: "volume",
69684
+ factor: 3.785411784,
69685
+ offset: 0
69686
+ },
69687
+ s: {
69688
+ dimension: "duration",
69689
+ factor: 1,
69690
+ offset: 0
69691
+ },
69692
+ ms: {
69693
+ dimension: "duration",
69694
+ factor: .001,
69695
+ offset: 0
69696
+ },
69697
+ min: {
69698
+ dimension: "duration",
69699
+ factor: 60,
69700
+ offset: 0
69701
+ },
69702
+ h: {
69703
+ dimension: "duration",
69704
+ factor: 3600,
69705
+ offset: 0
69706
+ },
69707
+ d: {
69708
+ dimension: "duration",
69709
+ factor: 86400,
69710
+ offset: 0
69711
+ },
69712
+ B: {
69713
+ dimension: "information",
69714
+ factor: 1,
69715
+ offset: 0
69716
+ },
69717
+ KiB: {
69718
+ dimension: "information",
69719
+ factor: 1024,
69720
+ offset: 0
69721
+ },
69722
+ MiB: {
69723
+ dimension: "information",
69724
+ factor: 1024 ** 2,
69725
+ offset: 0
69726
+ },
69727
+ GiB: {
69728
+ dimension: "information",
69729
+ factor: 1024 ** 3,
69730
+ offset: 0
69731
+ },
69732
+ TiB: {
69733
+ dimension: "information",
69734
+ factor: 1024 ** 4,
69735
+ offset: 0
69736
+ },
69737
+ kB: {
69738
+ dimension: "information",
69739
+ factor: 1e3,
69740
+ offset: 0
69741
+ },
69742
+ MB: {
69743
+ dimension: "information",
69744
+ factor: 1e6,
69745
+ offset: 0
69746
+ },
69747
+ GB: {
69748
+ dimension: "information",
69749
+ factor: 1e9,
69750
+ offset: 0
69751
+ },
69752
+ "°": {
69753
+ dimension: "angle",
69754
+ factor: 1,
69755
+ offset: 0
69756
+ },
69757
+ dBFS: {
69758
+ dimension: "sound-level",
69759
+ factor: 1,
69760
+ offset: 0
69761
+ },
69762
+ dBm: {
69763
+ dimension: "signal-level",
69764
+ factor: 1,
69765
+ offset: 0
69766
+ },
69767
+ mired: {
69768
+ dimension: "colour-temperature",
69769
+ factor: 1,
69770
+ offset: 0
69771
+ },
69772
+ kbps: {
69773
+ dimension: "bitrate",
69774
+ factor: 1,
69775
+ offset: 0
69776
+ },
69777
+ Mbps: {
69778
+ dimension: "bitrate",
69779
+ factor: 1e3,
69780
+ offset: 0
69781
+ },
69782
+ fps: {
69783
+ dimension: "rate",
69784
+ factor: 1,
69785
+ offset: 0
69786
+ }
69787
+ };
69788
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
69789
+ function specFor(unit) {
69790
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
69791
+ }
69792
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
69793
+ function unitDimension(unit) {
69794
+ return specFor(unit)?.dimension;
69795
+ }
69796
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
69797
+ function toCanonical(value, spec) {
69798
+ return value * spec.factor + spec.offset;
69799
+ }
69800
+ /** Canonical-unit value → the unit described by `spec`. */
69801
+ function fromCanonical(canonical, spec) {
69802
+ return (canonical - spec.offset) / spec.factor;
69803
+ }
69804
+ /**
69805
+ * Graceful variant for the render path: returns the converted value, or
69806
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
69807
+ * Never throws — a render caller falls back to the raw value + source unit.
69808
+ */
69809
+ function tryConvertUnit(value, from, to) {
69810
+ if (from === to) return value;
69811
+ const fromSpec = specFor(from);
69812
+ if (!fromSpec) return void 0;
69813
+ const toSpec = specFor(to);
69814
+ if (!toSpec) return void 0;
69815
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
69816
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
69817
+ }
68993
69818
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
68994
69819
  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;
68995
69820
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -69320,6 +70145,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69320
70145
  featureProbe: featureProbeCapability,
69321
70146
  flood: floodCapability,
69322
70147
  gas: gasCapability,
70148
+ groupCommands: groupCommandsCapability,
70149
+ groupStatus: groupStatusCapability,
69323
70150
  heatingValve: heatingValveCapability,
69324
70151
  humidifier: humidifierCapability,
69325
70152
  humiditySensor: humiditySensorCapability,
@@ -70696,6 +71523,13 @@ Object.freeze({
70696
71523
  access: "view",
70697
71524
  role: "admin"
70698
71525
  },
71526
+ "addons.getPackageDocs": {
71527
+ capName: "addons",
71528
+ capScope: "system",
71529
+ addonId: null,
71530
+ access: "view",
71531
+ role: "admin"
71532
+ },
70699
71533
  "addons.getVersions": {
70700
71534
  capName: "addons",
70701
71535
  capScope: "system",
@@ -73804,6 +74638,20 @@ Object.freeze({
73804
74638
  access: "view",
73805
74639
  role: "viewer"
73806
74640
  },
74641
+ "groupCommands.getStatus": {
74642
+ capName: "group-commands",
74643
+ capScope: "device",
74644
+ addonId: null,
74645
+ access: "view",
74646
+ role: "viewer"
74647
+ },
74648
+ "groupStatus.getStatus": {
74649
+ capName: "group-status",
74650
+ capScope: "device",
74651
+ addonId: null,
74652
+ access: "view",
74653
+ role: "viewer"
74654
+ },
73807
74655
  "heatingValve.getStatus": {
73808
74656
  capName: "heating-valve",
73809
74657
  capScope: "device",
@@ -83626,6 +84474,37 @@ var SENSOR_CAP_VOCABULARY = {
83626
84474
  icon: "flame"
83627
84475
  }]
83628
84476
  },
84477
+ "group-commands": {
84478
+ scope: "device",
84479
+ noun: t("Group command", "Comando di gruppo")
84480
+ },
84481
+ "group-status": {
84482
+ scope: "device",
84483
+ noun: t("Group", "Gruppo"),
84484
+ state: [
84485
+ {
84486
+ kind: "numeric",
84487
+ field: "included",
84488
+ quantity: t("the members reporting", "i membri che rispondono"),
84489
+ label: t("Reporting", "Rispondono"),
84490
+ icon: "users"
84491
+ },
84492
+ {
84493
+ kind: "numeric",
84494
+ field: "excluded",
84495
+ quantity: t("the members left out", "i membri esclusi"),
84496
+ label: t("Excluded", "Esclusi"),
84497
+ icon: "users"
84498
+ },
84499
+ {
84500
+ kind: "numeric",
84501
+ field: "total",
84502
+ quantity: t("the member count", "il numero di membri"),
84503
+ label: t("Members", "Membri"),
84504
+ icon: "sigma"
84505
+ }
84506
+ ]
84507
+ },
83629
84508
  "humidity-sensor": {
83630
84509
  scope: "device",
83631
84510
  noun: t("Humidity", "Umidità"),
@@ -83659,7 +84538,6 @@ var SENSOR_CAP_VOCABULARY = {
83659
84538
  field: "value",
83660
84539
  quantity: t("the reading", "il valore"),
83661
84540
  label: t("Reading", "Valore"),
83662
- unitField: "unit",
83663
84541
  icon: "gauge"
83664
84542
  }]
83665
84543
  },
@@ -83795,7 +84673,7 @@ var SENSOR_CAP_VOCABULARY = {
83795
84673
  field: "temperature",
83796
84674
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83797
84675
  label: t("Temperature", "Temperatura"),
83798
- unitField: "temperatureUnit",
84676
+ unit: "°C",
83799
84677
  icon: "thermometer"
83800
84678
  },
83801
84679
  {
@@ -83811,7 +84689,7 @@ var SENSOR_CAP_VOCABULARY = {
83811
84689
  field: "windSpeed",
83812
84690
  quantity: t("the wind speed", "la velocità del vento"),
83813
84691
  label: t("Wind", "Vento"),
83814
- unitField: "windSpeedUnit",
84692
+ unit: "m/s",
83815
84693
  icon: "wind"
83816
84694
  },
83817
84695
  {
@@ -83819,7 +84697,7 @@ var SENSOR_CAP_VOCABULARY = {
83819
84697
  field: "pressure",
83820
84698
  quantity: t("the air pressure", "la pressione atmosferica"),
83821
84699
  label: t("Pressure", "Pressione"),
83822
- unitField: "pressureUnit",
84700
+ unit: "hPa",
83823
84701
  icon: "gauge"
83824
84702
  }
83825
84703
  ]
@@ -85092,6 +85970,16 @@ Object.freeze({
85092
85970
  form: "single",
85093
85971
  optional: false
85094
85972
  }],
85973
+ "groupCommands.getStatus": [{
85974
+ name: "deviceId",
85975
+ form: "single",
85976
+ optional: false
85977
+ }],
85978
+ "groupStatus.getStatus": [{
85979
+ name: "deviceId",
85980
+ form: "single",
85981
+ optional: false
85982
+ }],
85095
85983
  "heatingValve.getStatus": [{
85096
85984
  name: "deviceId",
85097
85985
  form: "single",
@@ -87076,6 +87964,8 @@ new Map(Object.entries({
87076
87964
  "detection-blind": "device-events",
87077
87965
  "motion-noisy": "device-events",
87078
87966
  "motion-calm": "device-events",
87967
+ "package-llm-silent": "device-events",
87968
+ "package-llm-answered": "device-events",
87079
87969
  "recording-stalled": "device-events",
87080
87970
  "recording-healthy": "device-events",
87081
87971
  "recording-degraded": "device-events",
@@ -87159,6 +88049,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87159
88049
  label: "Motion without a subject",
87160
88050
  kinds: ["motion-noisy", "motion-calm"]
87161
88051
  },
88052
+ {
88053
+ id: "package-llm",
88054
+ family: "device-events",
88055
+ label: "Package confirm",
88056
+ kinds: ["package-llm-silent", "package-llm-answered"]
88057
+ },
87162
88058
  {
87163
88059
  id: "recording",
87164
88060
  family: "device-events",
@@ -87340,6 +88236,8 @@ var DEVICE_EVENTS = [
87340
88236
  "detection-blind",
87341
88237
  "motion-noisy",
87342
88238
  "motion-calm",
88239
+ "package-llm-silent",
88240
+ "package-llm-answered",
87343
88241
  "recording-stalled",
87344
88242
  "recording-healthy",
87345
88243
  "recording-degraded",
@@ -87608,6 +88506,15 @@ new Map([
87608
88506
  snapshotLabel: "Cameras with constant motion now",
87609
88507
  snapshotSection: null
87610
88508
  },
88509
+ {
88510
+ id: "package-llm-confirm",
88511
+ family: "device-events",
88512
+ label: "Package confirm (not answering / answering again)",
88513
+ bad: ["package-llm-silent"],
88514
+ recovery: ["package-llm-answered"],
88515
+ snapshotLabel: "Cameras whose package confirm is not answering",
88516
+ snapshotSection: null
88517
+ },
87611
88518
  {
87612
88519
  id: "recording-health",
87613
88520
  family: "device-events",
@@ -88184,6 +89091,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
88184
89091
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
88185
89092
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
88186
89093
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
89094
+ /**
89095
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
89096
+ * An unknown unit or a unit from another dimension is a refusal: the caller
89097
+ * logs it and publishes null (D393). This function never guesses.
89098
+ */
89099
+ var DIMENSION_QUANTITY = {
89100
+ temperature: "temperature",
89101
+ pressure: "pressure",
89102
+ speed: "speed",
89103
+ "precipitation-rate": "precipitation-rate",
89104
+ length: "length",
89105
+ area: "area",
89106
+ illuminance: "illuminance",
89107
+ irradiance: "irradiance",
89108
+ power: "power",
89109
+ "apparent-power": "apparent-power",
89110
+ energy: "energy",
89111
+ voltage: "voltage",
89112
+ current: "current",
89113
+ "concentration-volume": "concentration-volume",
89114
+ "concentration-mass": "concentration-mass",
89115
+ mass: "mass",
89116
+ percentage: "percentage",
89117
+ volume: "volume",
89118
+ duration: "duration",
89119
+ information: "information",
89120
+ angle: "angle",
89121
+ "sound-level": "sound-level",
89122
+ "signal-level": "signal-level",
89123
+ "colour-temperature": "colour-temperature",
89124
+ bitrate: "bitrate",
89125
+ rate: "rate"
89126
+ };
89127
+ /**
89128
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
89129
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
89130
+ * precipitation is chosen by the caller (a device class), not by the symbol.
89131
+ */
89132
+ function quantityFromUnit(rawUnit) {
89133
+ const unit = normalizeUnit(rawUnit);
89134
+ if (unit === void 0) return void 0;
89135
+ if (unit === "ppb") return "concentration-ppb";
89136
+ if (unit === "ms") return "duration-ms";
89137
+ const dimension = unitDimension(unit);
89138
+ if (dimension === void 0) return void 0;
89139
+ return DIMENSION_QUANTITY[dimension];
89140
+ }
89141
+ function ingestReading(input) {
89142
+ if (!Number.isFinite(input.value)) return {
89143
+ ok: false,
89144
+ reason: "non-finite"
89145
+ };
89146
+ if (input.quantity === "device") return {
89147
+ ok: false,
89148
+ reason: "unknown-unit"
89149
+ };
89150
+ const raw = input.rawUnit?.trim();
89151
+ if (raw === void 0 || raw.length === 0) return {
89152
+ ok: true,
89153
+ value: input.value
89154
+ };
89155
+ const unit = normalizeUnit(raw);
89156
+ if (unit === void 0) return {
89157
+ ok: false,
89158
+ reason: "unknown-unit"
89159
+ };
89160
+ const base = baseUnitOf(input.quantity);
89161
+ if (base === void 0) {
89162
+ if (unitDimension(unit) !== void 0) return {
89163
+ ok: false,
89164
+ reason: "cross-dimension"
89165
+ };
89166
+ return {
89167
+ ok: false,
89168
+ reason: "unknown-unit"
89169
+ };
89170
+ }
89171
+ const converted = tryConvertUnit(input.value, unit, base);
89172
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
89173
+ ok: false,
89174
+ reason: "unknown-unit"
89175
+ } : {
89176
+ ok: false,
89177
+ reason: "cross-dimension"
89178
+ };
89179
+ return {
89180
+ ok: true,
89181
+ value: converted
89182
+ };
89183
+ }
88187
89184
  var MB = 1024 * 1024;
88188
89185
  1024 * MB, 3072 * MB;
88189
89186
  //#endregion
@@ -88325,6 +89322,12 @@ Object.defineProperty(exports, "heatingValveCapability", {
88325
89322
  return heatingValveCapability;
88326
89323
  }
88327
89324
  });
89325
+ Object.defineProperty(exports, "ingestReading", {
89326
+ enumerable: true,
89327
+ get: function() {
89328
+ return ingestReading;
89329
+ }
89330
+ });
88328
89331
  Object.defineProperty(exports, "isSecretMarker", {
88329
89332
  enumerable: true,
88330
89333
  get: function() {
@@ -88367,6 +89370,12 @@ Object.defineProperty(exports, "powerMeterCapability", {
88367
89370
  return powerMeterCapability;
88368
89371
  }
88369
89372
  });
89373
+ Object.defineProperty(exports, "quantityFromUnit", {
89374
+ enumerable: true,
89375
+ get: function() {
89376
+ return quantityFromUnit;
89377
+ }
89378
+ });
88370
89379
  Object.defineProperty(exports, "record", {
88371
89380
  enumerable: true,
88372
89381
  get: function() {