@camstack/addon-provider-unraid 0.2.147 → 0.2.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region ../types/dist/event-category-DHXxB8t9.mjs
2
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -325,6 +325,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
325
325
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
326
326
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
327
327
  /**
328
+ * A camera's package LLM confirm has failed closed three times in a row
329
+ * (D931, 2026-10-26). One edge per camera until
330
+ * {@link PipelinePackageLlmAnswered}.
331
+ */
332
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
333
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
334
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
335
+ /**
328
336
  * Per-camera pipeline config was mutated by the orchestrator
329
337
  * (a settings change via `setAgentInferenceDevices` /
330
338
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5434,7 +5442,7 @@ var ZodIssueCode = {
5434
5442
  var ZodFirstPartyTypeKind;
5435
5443
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5436
5444
  //#endregion
5437
- //#region ../types/dist/sleep-BErAxhul.mjs
5445
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5438
5446
  /**
5439
5447
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5440
5448
  * window to float samples (D455).
@@ -7988,6 +7996,99 @@ var RecordKindDeclarationSchema = object({
7988
7996
  }).strict();
7989
7997
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7990
7998
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7999
+ var READING_QUANTITIES = [
8000
+ "temperature",
8001
+ "pressure",
8002
+ "speed",
8003
+ "precipitation",
8004
+ "precipitation-rate",
8005
+ "length",
8006
+ "area",
8007
+ "illuminance",
8008
+ "irradiance",
8009
+ "power",
8010
+ "energy",
8011
+ "voltage",
8012
+ "current",
8013
+ "apparent-power",
8014
+ "humidity",
8015
+ "percentage",
8016
+ "concentration-volume",
8017
+ "concentration-mass",
8018
+ "concentration-ppb",
8019
+ "volume",
8020
+ "duration",
8021
+ "duration-ms",
8022
+ "information",
8023
+ "sound-level",
8024
+ "signal-level",
8025
+ "colour-temperature",
8026
+ "angle",
8027
+ "bitrate",
8028
+ "rate",
8029
+ "mass",
8030
+ "index",
8031
+ "ratio",
8032
+ "count",
8033
+ "none",
8034
+ "device"
8035
+ ];
8036
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8037
+ new Set(READING_QUANTITIES);
8038
+ /**
8039
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8040
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8041
+ */
8042
+ var QUANTITY_BASE = {
8043
+ temperature: "°C",
8044
+ pressure: "hPa",
8045
+ speed: "m/s",
8046
+ precipitation: "mm",
8047
+ "precipitation-rate": "mm/h",
8048
+ length: "m",
8049
+ area: "m²",
8050
+ illuminance: "lx",
8051
+ irradiance: "W/m²",
8052
+ power: "W",
8053
+ energy: "kWh",
8054
+ voltage: "V",
8055
+ current: "A",
8056
+ "apparent-power": "VA",
8057
+ humidity: "%",
8058
+ percentage: "%",
8059
+ "concentration-volume": "ppm",
8060
+ "concentration-mass": "µg/m³",
8061
+ "concentration-ppb": "ppb",
8062
+ volume: "L",
8063
+ duration: "s",
8064
+ "duration-ms": "ms",
8065
+ information: "B",
8066
+ "sound-level": "dBFS",
8067
+ "signal-level": "dBm",
8068
+ "colour-temperature": "mired",
8069
+ angle: "°",
8070
+ bitrate: "kbps",
8071
+ rate: "fps",
8072
+ mass: "kg",
8073
+ index: "none",
8074
+ ratio: "none",
8075
+ count: "none",
8076
+ none: "none",
8077
+ device: "device"
8078
+ };
8079
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8080
+ function baseUnitOf(quantity) {
8081
+ const symbol = QUANTITY_BASE[quantity];
8082
+ if (symbol === "none" || symbol === "device") return void 0;
8083
+ return symbol;
8084
+ }
8085
+ function readingMeta(quantity, extras) {
8086
+ return {
8087
+ quantity,
8088
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8089
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8090
+ };
8091
+ }
7991
8092
  /** Shared Zod schemas used across streaming capabilities. */
7992
8093
  var CamProfileSchema = _enum([
7993
8094
  "high",
@@ -8015,8 +8116,8 @@ var CamStreamKindSchema = _enum([
8015
8116
  "derived"
8016
8117
  ]);
8017
8118
  var CamStreamResolutionSchema = object({
8018
- width: number().int().positive(),
8019
- height: number().int().positive()
8119
+ width: number().int().positive().meta(readingMeta("none")),
8120
+ height: number().int().positive().meta(readingMeta("none"))
8020
8121
  });
8021
8122
  var CameraStreamSchema = object({
8022
8123
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8534,6 +8635,8 @@ var CAP_NAMES = Object.freeze([
8534
8635
  "filesystem-browse",
8535
8636
  "flood",
8536
8637
  "gas",
8638
+ "group-commands",
8639
+ "group-status",
8537
8640
  "heating-valve",
8538
8641
  "humidifier",
8539
8642
  "humidity-sensor",
@@ -8663,14 +8766,6 @@ Object.freeze({
8663
8766
  {
8664
8767
  "path": "aqi",
8665
8768
  "valueType": "number"
8666
- },
8667
- {
8668
- "path": "unit",
8669
- "valueType": "string"
8670
- },
8671
- {
8672
- "path": "precision",
8673
- "valueType": "number"
8674
8769
  }
8675
8770
  ],
8676
8771
  "alarm-panel": [
@@ -8706,20 +8801,10 @@ Object.freeze({
8706
8801
  "valueType": "boolean"
8707
8802
  }
8708
8803
  ],
8709
- "ambient-light-sensor": [
8710
- {
8711
- "path": "lux",
8712
- "valueType": "number"
8713
- },
8714
- {
8715
- "path": "unit",
8716
- "valueType": "string"
8717
- },
8718
- {
8719
- "path": "precision",
8720
- "valueType": "number"
8721
- }
8722
- ],
8804
+ "ambient-light-sensor": [{
8805
+ "path": "lux",
8806
+ "valueType": "number"
8807
+ }],
8723
8808
  "audio-metrics": [
8724
8809
  {
8725
8810
  "path": "ts",
@@ -8799,6 +8884,10 @@ Object.freeze({
8799
8884
  "path": "lastUpdated",
8800
8885
  "valueType": "number"
8801
8886
  },
8887
+ {
8888
+ "path": "volts",
8889
+ "valueType": "number"
8890
+ },
8802
8891
  {
8803
8892
  "path": "lastContactAt",
8804
8893
  "valueType": "number"
@@ -9041,10 +9130,6 @@ Object.freeze({
9041
9130
  "datetime"
9042
9131
  ]
9043
9132
  },
9044
- {
9045
- "path": "unit",
9046
- "valueType": "string"
9047
- },
9048
9133
  {
9049
9134
  "path": "min",
9050
9135
  "valueType": "number"
@@ -9056,19 +9141,6 @@ Object.freeze({
9056
9141
  {
9057
9142
  "path": "step",
9058
9143
  "valueType": "number"
9059
- },
9060
- {
9061
- "path": "precision",
9062
- "valueType": "number"
9063
- },
9064
- {
9065
- "path": "format",
9066
- "valueType": "enum",
9067
- "enumValues": [
9068
- "date",
9069
- "time",
9070
- "datetime"
9071
- ]
9072
9144
  }
9073
9145
  ],
9074
9146
  "cover": [
@@ -9309,13 +9381,8 @@ Object.freeze({
9309
9381
  "path": "value",
9310
9382
  "valueType": "string"
9311
9383
  }, {
9312
- "path": "format",
9313
- "valueType": "enum",
9314
- "enumValues": [
9315
- "date",
9316
- "time",
9317
- "datetime"
9318
- ]
9384
+ "path": "timestamp",
9385
+ "valueType": "boolean"
9319
9386
  }],
9320
9387
  "event-emitter": [
9321
9388
  {
@@ -9388,6 +9455,20 @@ Object.freeze({
9388
9455
  "path": "detected",
9389
9456
  "valueType": "boolean"
9390
9457
  }],
9458
+ "group-status": [
9459
+ {
9460
+ "path": "included",
9461
+ "valueType": "number"
9462
+ },
9463
+ {
9464
+ "path": "excluded",
9465
+ "valueType": "number"
9466
+ },
9467
+ {
9468
+ "path": "total",
9469
+ "valueType": "number"
9470
+ }
9471
+ ],
9391
9472
  "heating-valve": [{
9392
9473
  "path": "position",
9393
9474
  "valueType": "number"
@@ -9422,20 +9503,10 @@ Object.freeze({
9422
9503
  "valueType": "number"
9423
9504
  }
9424
9505
  ],
9425
- "humidity-sensor": [
9426
- {
9427
- "path": "percent",
9428
- "valueType": "number"
9429
- },
9430
- {
9431
- "path": "unit",
9432
- "valueType": "string"
9433
- },
9434
- {
9435
- "path": "precision",
9436
- "valueType": "number"
9437
- }
9438
- ],
9506
+ "humidity-sensor": [{
9507
+ "path": "percent",
9508
+ "valueType": "number"
9509
+ }],
9439
9510
  "image": [{
9440
9511
  "path": "url",
9441
9512
  "valueType": "string"
@@ -9774,20 +9845,10 @@ Object.freeze({
9774
9845
  "valueType": "boolean"
9775
9846
  }
9776
9847
  ],
9777
- "numeric-sensor": [
9778
- {
9779
- "path": "value",
9780
- "valueType": "number"
9781
- },
9782
- {
9783
- "path": "unit",
9784
- "valueType": "string"
9785
- },
9786
- {
9787
- "path": "precision",
9788
- "valueType": "number"
9789
- }
9790
- ],
9848
+ "numeric-sensor": [{
9849
+ "path": "value",
9850
+ "valueType": "number"
9851
+ }],
9791
9852
  "pet-feeder": [
9792
9853
  {
9793
9854
  "path": "foodLevel",
@@ -9879,14 +9940,6 @@ Object.freeze({
9879
9940
  {
9880
9941
  "path": "powerFactor",
9881
9942
  "valueType": "number"
9882
- },
9883
- {
9884
- "path": "unit",
9885
- "valueType": "string"
9886
- },
9887
- {
9888
- "path": "precision",
9889
- "valueType": "number"
9890
9943
  }
9891
9944
  ],
9892
9945
  "presence": [
@@ -9915,20 +9968,10 @@ Object.freeze({
9915
9968
  "valueType": "number"
9916
9969
  }
9917
9970
  ],
9918
- "pressure-sensor": [
9919
- {
9920
- "path": "hpa",
9921
- "valueType": "number"
9922
- },
9923
- {
9924
- "path": "unit",
9925
- "valueType": "string"
9926
- },
9927
- {
9928
- "path": "precision",
9929
- "valueType": "number"
9930
- }
9931
- ],
9971
+ "pressure-sensor": [{
9972
+ "path": "hpa",
9973
+ "valueType": "number"
9974
+ }],
9932
9975
  "privacy-mask": [{
9933
9976
  "path": "enabled",
9934
9977
  "valueType": "boolean"
@@ -10239,20 +10282,10 @@ Object.freeze({
10239
10282
  "path": "tampered",
10240
10283
  "valueType": "boolean"
10241
10284
  }],
10242
- "temperature-sensor": [
10243
- {
10244
- "path": "celsius",
10245
- "valueType": "number"
10246
- },
10247
- {
10248
- "path": "unit",
10249
- "valueType": "string"
10250
- },
10251
- {
10252
- "path": "precision",
10253
- "valueType": "number"
10254
- }
10255
- ],
10285
+ "temperature-sensor": [{
10286
+ "path": "celsius",
10287
+ "valueType": "number"
10288
+ }],
10256
10289
  "update": [
10257
10290
  {
10258
10291
  "path": "currentVersion",
@@ -10363,10 +10396,6 @@ Object.freeze({
10363
10396
  "path": "temperature",
10364
10397
  "valueType": "number"
10365
10398
  },
10366
- {
10367
- "path": "temperatureUnit",
10368
- "valueType": "string"
10369
- },
10370
10399
  {
10371
10400
  "path": "humidity",
10372
10401
  "valueType": "number"
@@ -10375,18 +10404,10 @@ Object.freeze({
10375
10404
  "path": "pressure",
10376
10405
  "valueType": "number"
10377
10406
  },
10378
- {
10379
- "path": "pressureUnit",
10380
- "valueType": "string"
10381
- },
10382
10407
  {
10383
10408
  "path": "windSpeed",
10384
10409
  "valueType": "number"
10385
10410
  },
10386
- {
10387
- "path": "windSpeedUnit",
10388
- "valueType": "string"
10389
- },
10390
10411
  {
10391
10412
  "path": "windBearing",
10392
10413
  "valueType": "number"
@@ -10803,7 +10824,7 @@ function errMsg(err) {
10803
10824
  return String(err);
10804
10825
  }
10805
10826
  //#endregion
10806
- //#region ../types/dist/composition-kEl9mevu.mjs
10827
+ //#region ../types/dist/composition-OhpuespA.mjs
10807
10828
  /**
10808
10829
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10809
10830
  *
@@ -11558,15 +11579,74 @@ function validateExpressionSource(src) {
11558
11579
  }
11559
11580
  return null;
11560
11581
  }
11561
- /** A composed device's stableId is this prefix plus the block id. */
11562
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11563
- /** Longest snippet a `code` source may carry (slice 4). */
11564
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11582
+ /**
11583
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11584
+ * id. Split out so helper schemas can name it without a cycle through
11585
+ * `composition.ts` (D659).
11586
+ */
11565
11587
  var CompositionSourceRefSchema = object({
11566
11588
  /** The owning addon; `stableId` is unique only within it. */
11567
11589
  addonId: string().min(1),
11568
11590
  stableId: string().min(1)
11569
11591
  });
11592
+ var GROUP_TEMPLATE_IDS = [
11593
+ "temperature",
11594
+ "humidity",
11595
+ "pressure",
11596
+ "illuminance",
11597
+ "power",
11598
+ "energy",
11599
+ "battery",
11600
+ "numeric",
11601
+ "motion",
11602
+ "contact",
11603
+ "leak",
11604
+ "smoke",
11605
+ "occupancy",
11606
+ "binary",
11607
+ "switch",
11608
+ "light"
11609
+ ];
11610
+ var NUMERIC_AGGREGATES = [
11611
+ "mean",
11612
+ "median",
11613
+ "min",
11614
+ "max",
11615
+ "sum"
11616
+ ];
11617
+ var BINARY_AGGREGATES = ["any", "all"];
11618
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11619
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11620
+ var AGGREGATE_REDUCERS = [
11621
+ "mean",
11622
+ "median",
11623
+ "min",
11624
+ "max",
11625
+ "sum",
11626
+ "any",
11627
+ "all"
11628
+ ];
11629
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11630
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11631
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11632
+ /** One member. The field is stored so occupancy can name motion or binary. */
11633
+ var HelperMemberSchema = object({
11634
+ source: CompositionSourceRefSchema,
11635
+ cap: string().min(1),
11636
+ fieldPath: string().min(1)
11637
+ });
11638
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11639
+ kind: literal("group"),
11640
+ templateId: GroupTemplateIdSchema,
11641
+ aggregate: AggregateReducerSchema,
11642
+ members: array(HelperMemberSchema).min(1).max(64),
11643
+ /** Actuator groups only. Readings leave this unset. */
11644
+ active: ActiveCriterionSchema.optional()
11645
+ })]);
11646
+ /** A composed device's stableId is this prefix plus the block id. */
11647
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11648
+ /** Longest snippet a `code` source may carry (slice 4). */
11649
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11570
11650
  /** One field of one capability of one source device. */
11571
11651
  var CompositionFieldReadSchema = object({
11572
11652
  source: CompositionSourceRefSchema,
@@ -11610,7 +11690,33 @@ var CompositionCodeSourceSchema = object({
11610
11690
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11611
11691
  CompositionFromSourceSchema,
11612
11692
  CompositionExpressionSourceSchema,
11613
- CompositionCodeSourceSchema
11693
+ CompositionCodeSourceSchema,
11694
+ object({
11695
+ kind: literal("aggregate"),
11696
+ reducer: _enum([
11697
+ "mean",
11698
+ "median",
11699
+ "min",
11700
+ "max",
11701
+ "sum",
11702
+ "any",
11703
+ "all"
11704
+ ]),
11705
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11706
+ /** `value` is the reading; the others are the member counts. */
11707
+ publish: _enum([
11708
+ "value",
11709
+ "included",
11710
+ "excluded",
11711
+ "total"
11712
+ ]),
11713
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11714
+ empty: _enum([
11715
+ "null",
11716
+ "omit",
11717
+ "offline"
11718
+ ])
11719
+ })
11614
11720
  ]);
11615
11721
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11616
11722
  kind: literal("forward"),
@@ -11656,7 +11762,13 @@ var CompositionSchema = object({
11656
11762
  kind: literal("existing"),
11657
11763
  device: CompositionSourceRefSchema
11658
11764
  })]),
11659
- features: array(CompositionFeatureSchema).min(1).max(16)
11765
+ features: array(CompositionFeatureSchema).min(1).max(16),
11766
+ /**
11767
+ * Present when the block is a helper template. Absent on a free
11768
+ * composition (the room example, device 4603). The editor reads this,
11769
+ * never the compiled features.
11770
+ */
11771
+ helper: HelperSpecSchema.optional()
11660
11772
  }).superRefine((composition, ctx) => {
11661
11773
  const seen = /* @__PURE__ */ new Set();
11662
11774
  composition.features.forEach((feature, index) => {
@@ -17864,7 +17976,11 @@ var CompositionProblemSchema = object({
17864
17976
  "item-field-reserved",
17865
17977
  "target-missing",
17866
17978
  "target-unreadable",
17867
- "not-replaceable"
17979
+ "not-replaceable",
17980
+ "native-capability-replace",
17981
+ "mixed-quantity",
17982
+ "helper-mismatch",
17983
+ "member-constraint"
17868
17984
  ]),
17869
17985
  path: string(),
17870
17986
  message: string()
@@ -17885,7 +18001,8 @@ var CompositionUnavailableModeSchema = _enum([
17885
18001
  "null",
17886
18002
  "offline",
17887
18003
  "unknown",
17888
- "release"
18004
+ "release",
18005
+ "omit"
17889
18006
  ]);
17890
18007
  /**
17891
18008
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17922,9 +18039,11 @@ var CompositionFieldPlanSchema = object({
17922
18039
  reads: array(CompositionFieldReadSchema)
17923
18040
  });
17924
18041
  /**
17925
- * What a feature does to its target (D663): `new` builds a capability on a new
17926
- * device; on an existing device it `add`s a capability the device lacks, or
17927
- * `replace`s named fields of one it has natively.
18042
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18043
+ * new device; on an existing device it `add`s a capability the device lacks.
18044
+ * Replacing a capability the device already serves is refused
18045
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18046
+ * wire for a claim the hub already holds.
17928
18047
  */
17929
18048
  var CompositionFeatureModeSchema = _enum([
17930
18049
  "new",
@@ -18097,6 +18216,275 @@ Object.freeze({
18097
18216
  result: ["number", "null"]
18098
18217
  }
18099
18218
  });
18219
+ /**
18220
+ * One catalog row per base reading a group helper can build (D944 §3).
18221
+ * Data, not a form default written into the block.
18222
+ */
18223
+ var NOW = {
18224
+ fieldPath: "lastFetchedAt",
18225
+ expr: "now"
18226
+ };
18227
+ var CHANGED = {
18228
+ fieldPath: "lastChangedAt",
18229
+ expr: "now"
18230
+ };
18231
+ function numeric(input) {
18232
+ return {
18233
+ ...input,
18234
+ family: "numeric",
18235
+ deviceType: DeviceType.Sensor,
18236
+ memberDeviceType: DeviceType.Sensor,
18237
+ commandCaps: [],
18238
+ baseUnit: baseUnitOf(input.quantity),
18239
+ aggregates: NUMERIC_AGGREGATES,
18240
+ accept: [{
18241
+ cap: input.outputCap,
18242
+ fieldPath: input.outputField
18243
+ }]
18244
+ };
18245
+ }
18246
+ function binary(input) {
18247
+ return {
18248
+ ...input,
18249
+ family: "binary",
18250
+ deviceType: DeviceType.Sensor,
18251
+ memberDeviceType: DeviceType.Sensor,
18252
+ commandCaps: [],
18253
+ quantity: "none",
18254
+ baseUnit: void 0,
18255
+ aggregates: BINARY_AGGREGATES,
18256
+ empty: "offline"
18257
+ };
18258
+ }
18259
+ var GROUP_TEMPLATES = [
18260
+ numeric({
18261
+ id: "temperature",
18262
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18263
+ role: DeviceRole.TemperatureSensor,
18264
+ outputCap: "temperature-sensor",
18265
+ outputField: "celsius",
18266
+ quantity: "temperature",
18267
+ defaultAggregate: "mean",
18268
+ empty: "null",
18269
+ companions: [NOW]
18270
+ }),
18271
+ numeric({
18272
+ id: "humidity",
18273
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18274
+ role: DeviceRole.HumiditySensor,
18275
+ outputCap: "humidity-sensor",
18276
+ outputField: "percent",
18277
+ quantity: "humidity",
18278
+ defaultAggregate: "mean",
18279
+ empty: "null",
18280
+ companions: [NOW]
18281
+ }),
18282
+ numeric({
18283
+ id: "pressure",
18284
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18285
+ role: DeviceRole.PressureSensor,
18286
+ outputCap: "pressure-sensor",
18287
+ outputField: "hpa",
18288
+ quantity: "pressure",
18289
+ defaultAggregate: "mean",
18290
+ empty: "null",
18291
+ companions: [NOW]
18292
+ }),
18293
+ numeric({
18294
+ id: "illuminance",
18295
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18296
+ role: DeviceRole.AmbientLightSensor,
18297
+ outputCap: "ambient-light-sensor",
18298
+ outputField: "lux",
18299
+ quantity: "illuminance",
18300
+ defaultAggregate: "mean",
18301
+ empty: "null",
18302
+ companions: [NOW]
18303
+ }),
18304
+ numeric({
18305
+ id: "power",
18306
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18307
+ role: DeviceRole.PowerSensor,
18308
+ outputCap: "power-meter",
18309
+ outputField: "watts",
18310
+ quantity: "power",
18311
+ defaultAggregate: "sum",
18312
+ empty: "omit",
18313
+ companions: [NOW]
18314
+ }),
18315
+ numeric({
18316
+ id: "energy",
18317
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18318
+ role: DeviceRole.EnergySensor,
18319
+ outputCap: "power-meter",
18320
+ outputField: "kwhTotal",
18321
+ quantity: "energy",
18322
+ defaultAggregate: "sum",
18323
+ empty: "omit",
18324
+ companions: [NOW]
18325
+ }),
18326
+ numeric({
18327
+ id: "battery",
18328
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18329
+ role: DeviceRole.BatterySensor,
18330
+ outputCap: "battery",
18331
+ outputField: "percentage",
18332
+ quantity: "percentage",
18333
+ defaultAggregate: "mean",
18334
+ empty: "null",
18335
+ companions: [
18336
+ {
18337
+ fieldPath: "charging",
18338
+ expr: "\"unknown\""
18339
+ },
18340
+ {
18341
+ fieldPath: "sleeping",
18342
+ expr: "false"
18343
+ },
18344
+ {
18345
+ fieldPath: "lastUpdated",
18346
+ expr: "now"
18347
+ }
18348
+ ]
18349
+ }),
18350
+ numeric({
18351
+ id: "numeric",
18352
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18353
+ role: DeviceRole.NumericSensor,
18354
+ outputCap: "numeric-sensor",
18355
+ outputField: "value",
18356
+ quantity: "device",
18357
+ defaultAggregate: "mean",
18358
+ empty: "null",
18359
+ companions: [NOW]
18360
+ }),
18361
+ binary({
18362
+ id: "motion",
18363
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18364
+ role: DeviceRole.MotionSensor,
18365
+ outputCap: "motion",
18366
+ outputField: "detected",
18367
+ defaultAggregate: "any",
18368
+ accept: [{
18369
+ cap: "motion",
18370
+ fieldPath: "detected"
18371
+ }],
18372
+ companions: [{
18373
+ fieldPath: "lastDetectedAt",
18374
+ expr: "null"
18375
+ }, {
18376
+ fieldPath: "autoClearAfterMs",
18377
+ expr: "null"
18378
+ }]
18379
+ }),
18380
+ binary({
18381
+ id: "contact",
18382
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18383
+ role: DeviceRole.ContactSensor,
18384
+ outputCap: "contact",
18385
+ outputField: "entryOpen",
18386
+ defaultAggregate: "any",
18387
+ accept: [{
18388
+ cap: "contact",
18389
+ fieldPath: "entryOpen"
18390
+ }],
18391
+ companions: [CHANGED]
18392
+ }),
18393
+ binary({
18394
+ id: "leak",
18395
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18396
+ role: DeviceRole.LeakSensor,
18397
+ outputCap: "flood",
18398
+ outputField: "flooded",
18399
+ defaultAggregate: "any",
18400
+ accept: [{
18401
+ cap: "flood",
18402
+ fieldPath: "flooded"
18403
+ }],
18404
+ companions: [CHANGED]
18405
+ }),
18406
+ binary({
18407
+ id: "smoke",
18408
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18409
+ role: DeviceRole.SmokeSensor,
18410
+ outputCap: "smoke",
18411
+ outputField: "detected",
18412
+ defaultAggregate: "any",
18413
+ accept: [{
18414
+ cap: "smoke",
18415
+ fieldPath: "detected"
18416
+ }],
18417
+ companions: [CHANGED]
18418
+ }),
18419
+ binary({
18420
+ id: "occupancy",
18421
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18422
+ role: DeviceRole.BinarySensor,
18423
+ outputCap: "binary",
18424
+ outputField: "on",
18425
+ defaultAggregate: "any",
18426
+ accept: [{
18427
+ cap: "motion",
18428
+ fieldPath: "detected"
18429
+ }, {
18430
+ cap: "binary",
18431
+ fieldPath: "on"
18432
+ }],
18433
+ companions: [CHANGED]
18434
+ }),
18435
+ binary({
18436
+ id: "binary",
18437
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18438
+ role: DeviceRole.BinarySensor,
18439
+ outputCap: "binary",
18440
+ outputField: "on",
18441
+ defaultAggregate: "any",
18442
+ accept: [{
18443
+ cap: "binary",
18444
+ fieldPath: "on"
18445
+ }],
18446
+ companions: [CHANGED]
18447
+ }),
18448
+ actuator({
18449
+ id: "switch",
18450
+ 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.",
18451
+ deviceType: DeviceType.Switch,
18452
+ outputCap: "switch",
18453
+ commandCaps: ["switch"]
18454
+ }),
18455
+ actuator({
18456
+ id: "light",
18457
+ 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.",
18458
+ deviceType: DeviceType.Light,
18459
+ outputCap: "switch",
18460
+ commandCaps: [
18461
+ "switch",
18462
+ "brightness",
18463
+ "color"
18464
+ ]
18465
+ })
18466
+ ];
18467
+ function actuator(input) {
18468
+ return {
18469
+ ...input,
18470
+ family: "actuator",
18471
+ role: void 0,
18472
+ memberDeviceType: input.deviceType,
18473
+ outputField: "on",
18474
+ quantity: "none",
18475
+ baseUnit: void 0,
18476
+ aggregates: BINARY_AGGREGATES,
18477
+ defaultAggregate: "any",
18478
+ empty: "offline",
18479
+ accept: [{
18480
+ cap: "switch",
18481
+ fieldPath: "on"
18482
+ }],
18483
+ companions: [CHANGED]
18484
+ };
18485
+ }
18486
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18487
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18100
18488
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18101
18489
  /**
18102
18490
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19923,12 +20311,12 @@ function buildDeviceAdminUrl(parts) {
19923
20311
  * providers detect renames by matching `uniqueId` and patch `id`
19924
20312
  * in-place so dispatch keeps working.
19925
20313
  *
19926
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19927
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19928
- * state push and are the single source of truth. The `device_class` tag is
19929
- * an HA-specific input that is consumed at the discovery boundary
19930
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19931
- * stored after that point.
20314
+ * **Display unit and precision are NOT stored here.** They are a read-time
20315
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20316
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20317
+ * neither is repeated on each sample. The `device_class` tag is an
20318
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20319
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19932
20320
  *
19933
20321
  * The `raw` escape hatch carries provider-specific attributes that don't
19934
20322
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19942,6 +20330,14 @@ var SourceInfoSchema = object({
19942
20330
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19943
20331
  * Used to detect rename when `id` changes. */
19944
20332
  uniqueId: string().optional(),
20333
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20334
+ quantity: ReadingQuantitySchema.optional(),
20335
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20336
+ dateTimeShape: _enum([
20337
+ "date",
20338
+ "time",
20339
+ "datetime"
20340
+ ]).optional(),
19945
20341
  /** Provider-specific extras that don't fit the structured slots.
19946
20342
  * Whitelist what goes here per provider — do NOT dump entire HA
19947
20343
  * attribute blobs (would explode the meta JSON column). */
@@ -41046,7 +41442,8 @@ method(object({
41046
41442
  "enum"
41047
41443
  ]),
41048
41444
  enumValues: array(string()).optional(),
41049
- item: boolean().optional()
41445
+ item: boolean().optional(),
41446
+ quantity: ReadingQuantitySchema.optional()
41050
41447
  })).readonly(),
41051
41448
  itemArray: object({
41052
41449
  path: string(),
@@ -44890,8 +45287,8 @@ var MaskPolygonShapeSchema = object({
44890
45287
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44891
45288
  var MaskGridShapeSchema = object({
44892
45289
  kind: literal("grid"),
44893
- gridWidth: number(),
44894
- gridHeight: number(),
45290
+ gridWidth: number().meta(readingMeta("none")),
45291
+ gridHeight: number().meta(readingMeta("none")),
44895
45292
  cells: array(boolean())
44896
45293
  });
44897
45294
  discriminatedUnion("kind", [
@@ -45003,6 +45400,8 @@ var NcSystemEventKindSchema = _enum([
45003
45400
  "detection-blind",
45004
45401
  "motion-noisy",
45005
45402
  "motion-calm",
45403
+ "package-llm-silent",
45404
+ "package-llm-answered",
45006
45405
  "recording-stalled",
45007
45406
  "recording-healthy",
45008
45407
  "recording-degraded",
@@ -56094,6 +56493,28 @@ var PackageVersionInfoSchema = object({
56094
56493
  deprecated: string().optional(),
56095
56494
  distTags: array(string()).readonly()
56096
56495
  });
56496
+ /**
56497
+ * One published document. `ok` is markdown from the registry. `missing` means
56498
+ * the registry answered and the file was not there. `error` is a registry or
56499
+ * tarball failure — never an empty string standing in for either.
56500
+ */
56501
+ var PackageDocBodySchema = discriminatedUnion("status", [
56502
+ object({
56503
+ status: literal("ok"),
56504
+ markdown: string()
56505
+ }),
56506
+ object({ status: literal("missing") }),
56507
+ object({
56508
+ status: literal("error"),
56509
+ message: string()
56510
+ })
56511
+ ]);
56512
+ var PackageDocsSchema = object({
56513
+ name: string(),
56514
+ version: string(),
56515
+ readme: PackageDocBodySchema,
56516
+ changelog: PackageDocBodySchema
56517
+ });
56097
56518
  var SearchResultSchema = object({
56098
56519
  name: string(),
56099
56520
  version: string(),
@@ -56312,7 +56733,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56312
56733
  }), object({ success: literal(true) }), {
56313
56734
  kind: "mutation",
56314
56735
  auth: "admin"
56315
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56736
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56737
+ name: string().min(1),
56738
+ version: string().min(1).optional()
56739
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56316
56740
  kind: "mutation",
56317
56741
  auth: "admin"
56318
56742
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56371,27 +56795,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56371
56795
  */
56372
56796
  var AirQualitySensorStatusSchema = object({
56373
56797
  /** Carbon dioxide concentration in ppm. */
56374
- co2Ppm: number().min(0).optional(),
56798
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56375
56799
  /** Total volatile organic compounds in ppb. */
56376
- vocPpb: number().min(0).optional(),
56800
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56377
56801
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56378
- pm25: number().min(0).optional(),
56802
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56379
56803
  /** Particulate matter ≤ 10 μm in µg/m³. */
56380
- pm10: number().min(0).optional(),
56804
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56381
56805
  /** Composite AQI value (typically 0..500). */
56382
- aqi: number().optional(),
56806
+ aqi: number().optional().meta(readingMeta("index")),
56383
56807
  /** Ms epoch when the slice was last updated. */
56384
- lastFetchedAt: number(),
56385
- /** Live display unit of the single metric this slice carries (e.g. HA
56386
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56387
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56388
- * per slice is unambiguous. */
56389
- unit: string().optional(),
56390
- /** Suggested decimal places for numeric display.
56391
- * Populated live from the upstream source when provided (e.g. HA
56392
- * `attributes.suggested_display_precision`). Falls back to
56393
- * auto-formatting when absent. */
56394
- precision: number().int().min(0).max(10).optional()
56808
+ lastFetchedAt: number()
56395
56809
  });
56396
56810
  var airQualitySensorCapability = {
56397
56811
  name: "air-quality-sensor",
@@ -56428,12 +56842,12 @@ var airQualitySensorCapability = {
56428
56842
  },
56429
56843
  pm25: {
56430
56844
  kind: "gauge",
56431
- unit: "ug/m3",
56845
+ unit: "µg/m³",
56432
56846
  deadband: 1
56433
56847
  },
56434
56848
  pm10: {
56435
56849
  kind: "gauge",
56436
- unit: "ug/m3",
56850
+ unit: "µg/m³",
56437
56851
  deadband: 1
56438
56852
  },
56439
56853
  aqi: {
@@ -56451,22 +56865,13 @@ var airQualitySensorCapability = {
56451
56865
  * entries with `device_class: illuminance`.
56452
56866
  */
56453
56867
  var AmbientLightSensorStatusSchema = object({
56454
- /** Current illuminance in lux (lx). */
56455
- lux: number().min(0).meta({
56456
- unit: "lx",
56868
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56869
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56870
+ precision: 0,
56457
56871
  label: "Illuminance"
56458
- }),
56872
+ })),
56459
56873
  /** Ms epoch when the slice was last updated. */
56460
- lastFetchedAt: number(),
56461
- /** Live display unit from the upstream source (e.g. HA
56462
- * `attributes.unit_of_measurement`). The UI prefers this over the
56463
- * role's canonical unit. Absent → fall back to the canonical unit. */
56464
- unit: string().optional(),
56465
- /** Suggested decimal places for numeric display.
56466
- * Populated live from the upstream source when provided (e.g. HA
56467
- * `attributes.suggested_display_precision`). Falls back to
56468
- * auto-formatting when absent. */
56469
- precision: number().int().min(0).max(10).optional()
56874
+ lastFetchedAt: number()
56470
56875
  });
56471
56876
  var ambientLightSensorCapability = {
56472
56877
  name: "ambient-light-sensor",
@@ -56498,12 +56903,7 @@ var ambientLightSensorCapability = {
56498
56903
  } } },
56499
56904
  /** Clock fields: written, but excluded from the compare that decides
56500
56905
  * whether persisting is worth a SQLite commit. */
56501
- volatileStateFields: ["lastFetchedAt"],
56502
- customize: {
56503
- version: 1,
56504
- advanced: "derive",
56505
- targets: []
56506
- }
56906
+ volatileStateFields: ["lastFetchedAt"]
56507
56907
  };
56508
56908
  /**
56509
56909
  * Per-class audio metrics aggregated over a sliding window.
@@ -56730,10 +57130,7 @@ var BatteryStatusSchema = object({
56730
57130
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56731
57131
  * declines to band a non-finite reading, which is the correct shape.
56732
57132
  */
56733
- percentage: number().min(0).max(100).nullable().meta({
56734
- unit: "%",
56735
- label: "Charge"
56736
- }),
57133
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56737
57134
  /**
56738
57135
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56739
57136
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56760,7 +57157,14 @@ var BatteryStatusSchema = object({
56760
57157
  */
56761
57158
  sleeping: boolean().meta({ label: "Sleeping" }),
56762
57159
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56763
- lastUpdated: number(),
57160
+ lastUpdated: number().meta(readingMeta("none")),
57161
+ /**
57162
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57163
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57164
+ * that only report charge. A second reading on this cap, not a second
57165
+ * `numeric-sensor` on the same device.
57166
+ */
57167
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56764
57168
  /**
56765
57169
  * Ms epoch of the last time the device PROVED it was reachable — a
56766
57170
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56812,7 +57216,6 @@ var BATTERY_DEVICE_TYPES = [
56812
57216
  /** Generic Customize target for the percentage reading (D936). */
56813
57217
  var batteryCustomize = {
56814
57218
  version: 1,
56815
- advanced: "derive",
56816
57219
  targets: [{
56817
57220
  id: "percentage",
56818
57221
  cardinality: "scalar",
@@ -56821,7 +57224,7 @@ var batteryCustomize = {
56821
57224
  stateField: "percentage"
56822
57225
  },
56823
57226
  deviceTypes: BATTERY_DEVICE_TYPES,
56824
- apply: "add-or-replace",
57227
+ apply: "add",
56825
57228
  path: "percentage",
56826
57229
  sources: [{
56827
57230
  kind: "field",
@@ -56942,10 +57345,16 @@ onStatusChanged: { data: object({
56942
57345
  /**
56943
57346
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56944
57347
  */
56945
- history: { fields: { percentage: {
56946
- kind: "gauge",
56947
- unit: "%"
56948
- } } },
57348
+ history: { fields: {
57349
+ percentage: {
57350
+ kind: "gauge",
57351
+ unit: "%"
57352
+ },
57353
+ volts: {
57354
+ kind: "gauge",
57355
+ unit: "V"
57356
+ }
57357
+ } },
56949
57358
  customize: batteryCustomize,
56950
57359
  /** Clock fields: written, but excluded from the compare that decides
56951
57360
  * whether persisting is worth a SQLite commit. */
@@ -56992,12 +57401,7 @@ var binaryCapability = {
56992
57401
  history: { fields: { on: { kind: "boolean" } } },
56993
57402
  /** Clock fields: written, but excluded from the compare that decides
56994
57403
  * whether persisting is worth a SQLite commit. */
56995
- volatileStateFields: ["lastChangedAt"],
56996
- customize: {
56997
- version: 1,
56998
- advanced: "derive",
56999
- targets: []
57000
- }
57404
+ volatileStateFields: ["lastChangedAt"]
57001
57405
  };
57002
57406
  /**
57003
57407
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57013,7 +57417,7 @@ var binaryCapability = {
57013
57417
  */
57014
57418
  var BrightnessStatusSchema = object({
57015
57419
  /** Current level as 0..100 inclusive. Firmware-reported. */
57016
- percentage: number().min(0).max(100),
57420
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57017
57421
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57018
57422
  lastChangedAt: number()
57019
57423
  });
@@ -57484,21 +57888,21 @@ var ClimateControlStatusSchema = object({
57484
57888
  /** Single setpoint in Celsius. Used by single-target modes
57485
57889
  * (heat / cool / dry). Null when the device is in a range mode
57486
57890
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57487
- target: number().nullable(),
57891
+ target: number().nullable().meta(readingMeta("temperature")),
57488
57892
  /** Upper bound of the dual setpoint in Celsius. Populated only
57489
57893
  * in `heat_cool` mode. */
57490
- targetHigh: number().nullable(),
57894
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57491
57895
  /** Lower bound of the dual setpoint in Celsius. Populated only
57492
57896
  * in `heat_cool` mode. */
57493
- targetLow: number().nullable(),
57897
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57494
57898
  /** Target relative humidity (0..100). Null when the device has no
57495
57899
  * humidity control. */
57496
- targetHumidity: number().min(0).max(100).nullable(),
57900
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57497
57901
  /** Current measured temperature in Celsius. Read-only — pushed by
57498
57902
  * the device's internal sensor. */
57499
- currentTemp: number().nullable(),
57903
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57500
57904
  /** Current measured relative humidity (0..100). Read-only. */
57501
- currentHumidity: number().min(0).max(100).nullable(),
57905
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57502
57906
  /** Vertical louver swing on/off. Null when the device has no vertical
57503
57907
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57504
57908
  swingVertical: boolean().nullable(),
@@ -57506,11 +57910,11 @@ var ClimateControlStatusSchema = object({
57506
57910
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57507
57911
  swingHorizontal: boolean().nullable(),
57508
57912
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57509
- minTemp: number().nullable().optional(),
57913
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57510
57914
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57511
- maxTemp: number().nullable().optional(),
57915
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57512
57916
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57513
- tempStep: number().positive().nullable().optional(),
57917
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57514
57918
  /** Ms epoch when the slice was last updated (push or command). */
57515
57919
  lastFetchedAt: number()
57516
57920
  });
@@ -57652,17 +58056,17 @@ var climateControlCapability = {
57652
58056
  * modes to expose in the picker without a slice fetch.
57653
58057
  */
57654
58058
  var RgbTripletSchema = object({
57655
- r: number().int().min(0).max(255),
57656
- g: number().int().min(0).max(255),
57657
- b: number().int().min(0).max(255)
58059
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58060
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58061
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57658
58062
  });
57659
58063
  var HsvTripletSchema = object({
57660
58064
  /** Hue in degrees, 0..360. */
57661
- h: number().min(0).max(360),
58065
+ h: number().min(0).max(360).meta(readingMeta("none")),
57662
58066
  /** Saturation as 0..100 inclusive. */
57663
- s: number().min(0).max(100),
58067
+ s: number().min(0).max(100).meta(readingMeta("none")),
57664
58068
  /** Value/brightness as 0..100 inclusive. */
57665
- v: number().min(0).max(100)
58069
+ v: number().min(0).max(100).meta(readingMeta("none"))
57666
58070
  });
57667
58071
  var ColorInputSchema = discriminatedUnion("mode", [
57668
58072
  object({
@@ -57691,7 +58095,7 @@ var ColorStatusSchema = object({
57691
58095
  /** Populated when `mode === 'hsv'`. */
57692
58096
  hsv: HsvTripletSchema.optional(),
57693
58097
  /** Populated when `mode === 'mired'`. */
57694
- mireds: number().int().optional(),
58098
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57695
58099
  /** Ms epoch of the last operator-driven change. */
57696
58100
  lastChangedAt: number()
57697
58101
  });
@@ -57886,18 +58290,12 @@ _enum([
57886
58290
  "empty",
57887
58291
  "full"
57888
58292
  ]);
57889
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57890
- * each. */
57891
- var ConsumableRemainingUnitSchema = _enum([
57892
- "days",
57893
- "hours",
57894
- "cycles"
57895
- ]);
57896
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57897
- * days). `value` null = unknown (D393). */
58293
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58294
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58295
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57898
58296
  var ConsumableRemainingSchema = object({
57899
- value: number().min(0).nullable(),
57900
- unit: ConsumableRemainingUnitSchema
58297
+ value: number().min(0).nullable().meta(readingMeta("device")),
58298
+ kind: ConsumableRemainingKindSchema
57901
58299
  });
57902
58300
  /** A single consumable item. Either a continuous `level` (remaining
57903
58301
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57910,7 +58308,7 @@ var ConsumableItemSchema = object({
57910
58308
  /** Display name. */
57911
58309
  label: string().min(1),
57912
58310
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57913
- level: number().min(0).max(100).nullable(),
58311
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57914
58312
  /** The provider's discrete verdict when known (binary mode). */
57915
58313
  status: ConsumableStatusSchema.nullable(),
57916
58314
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57992,8 +58390,8 @@ var consumablesCustomize = {
57992
58390
  transforms: [{ kind: "identity" }]
57993
58391
  }],
57994
58392
  literals: [{
57995
- path: "remaining.unit",
57996
- values: [...ConsumableRemainingUnitSchema.options]
58393
+ path: "remaining.kind",
58394
+ values: [...ConsumableRemainingKindSchema.options]
57997
58395
  }]
57998
58396
  },
57999
58397
  {
@@ -58122,7 +58520,6 @@ reset: method(object({
58122
58520
  },
58123
58521
  "remaining.value": {
58124
58522
  kind: "gauge",
58125
- unitFrom: "remaining.unit",
58126
58523
  deadband: 1,
58127
58524
  displayName: "Remaining"
58128
58525
  }
@@ -58192,10 +58589,9 @@ var contactCapability = {
58192
58589
  * (empty array for the other kinds). It lives in the slice because
58193
58590
  * HA `select.*` entities may update their options at runtime.
58194
58591
  *
58195
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58196
- * `kind === 'numeric'` and are absent (undefined) for all other
58197
- * kinds. They are populated live from HA attributes on each state
58198
- * push — consistent with `options` and the sensor-unit approach.
58592
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58593
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58594
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58199
58595
  */
58200
58596
  var ControlStatusSchema = object({
58201
58597
  kind: _enum([
@@ -58207,39 +58603,17 @@ var ControlStatusSchema = object({
58207
58603
  /** Current value. Numeric kind → number; select/text/datetime → string.
58208
58604
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58209
58605
  * or time-only — HA accepts all three forms). */
58210
- value: union([number(), string()]),
58606
+ value: union([number().meta(readingMeta("device")), string()]),
58211
58607
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58212
58608
  options: array(string()),
58213
58609
  /** Ms epoch when the slice was last updated. */
58214
58610
  lastChangedAt: number(),
58215
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58216
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58217
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58218
- unit: string().optional(),
58219
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58220
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58221
- min: number().optional(),
58222
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58223
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58224
- max: number().optional(),
58225
- /** Step increment. Populated live from HA `attributes.step`.
58226
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58227
- step: number().optional(),
58228
- /** Suggested decimal places for numeric display. Populated live from HA
58229
- * `attributes.suggested_display_precision`, or derived from `step` when the
58230
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58231
- * other kinds. Falls back to auto-formatting when absent. */
58232
- precision: number().int().min(0).max(10).optional(),
58233
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58234
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58235
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58236
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58237
- * `has_date`/`has_time` attributes on each push. */
58238
- format: _enum([
58239
- "date",
58240
- "time",
58241
- "datetime"
58242
- ]).optional()
58611
+ /** Minimum allowed value, in the device quantity's base unit. */
58612
+ min: number().optional().meta(readingMeta("device")),
58613
+ /** Maximum allowed value, in the device quantity's base unit. */
58614
+ max: number().optional().meta(readingMeta("device")),
58615
+ /** Step increment, in the device quantity's base unit. */
58616
+ step: number().optional().meta(readingMeta("device"))
58243
58617
  });
58244
58618
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58245
58619
  object({
@@ -58305,9 +58679,9 @@ var CoverStatusSchema = object({
58305
58679
  ]),
58306
58680
  /** 0 = fully closed, 100 = fully open. Null when the device has
58307
58681
  * no intermediate position surface. */
58308
- position: number().min(0).max(100).nullable(),
58682
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58309
58683
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58310
- tiltPosition: number().min(0).max(100).nullable(),
58684
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58311
58685
  /** Ms epoch when the slice was last updated. */
58312
58686
  lastChangedAt: number()
58313
58687
  });
@@ -58410,9 +58784,9 @@ var NormalizedRangeSchema$1 = object({
58410
58784
  var DayNightStatusSchema = object({
58411
58785
  mode: DayNightModeSchema,
58412
58786
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58413
- sensitivity: number().optional(),
58787
+ sensitivity: number().optional().meta(readingMeta("none")),
58414
58788
  /** Delay before the IR-cut filter flips, in seconds. */
58415
- switchDelaySec: number().optional(),
58789
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58416
58790
  lastFetchedAt: number()
58417
58791
  });
58418
58792
  /**
@@ -58630,11 +59004,11 @@ var DishwasherStatusSchema = object({
58630
59004
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58631
59005
  availablePrograms: array(string()),
58632
59006
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58633
- remainingSeconds: number().int().nonnegative().nullable(),
59007
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58634
59008
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58635
- delayedStartSeconds: number().int().nonnegative().nullable(),
59009
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58636
59010
  /** Cycle completion 0..100, or null when not applicable. */
58637
- progressPercent: number().min(0).max(100).nullable(),
59011
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58638
59012
  /** True when the door is reported open; null when the door state is not known. */
58639
59013
  doorOpen: boolean().nullable(),
58640
59014
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58658,7 +59032,7 @@ var DishwasherStatusSchema = object({
58658
59032
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58659
59033
  machineCareDue: boolean().nullable(),
58660
59034
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58661
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59035
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58662
59036
  /** Option states: null when the appliance does not expose the option or has not said. */
58663
59037
  halfLoad: boolean().nullable(),
58664
59038
  hygienePlus: boolean().nullable(),
@@ -58673,7 +59047,7 @@ var DishwasherStatusSchema = object({
58673
59047
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58674
59048
  activeEvents: array(string()),
58675
59049
  /** Numeric error code when the appliance reports one; null otherwise. */
58676
- errorCode: number().nullable(),
59050
+ errorCode: number().nullable().meta(readingMeta("none")),
58677
59051
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58678
59052
  errorLabel: string().nullable(),
58679
59053
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58904,7 +59278,7 @@ var DoorbellStatusSchema = object({
58904
59278
  /** Ms epoch of the last press. null = never observed since this provider started. */
58905
59279
  lastPressedAt: number().nullable(),
58906
59280
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58907
- pressCountSinceStart: number()
59281
+ pressCountSinceStart: number().meta(readingMeta("count"))
58908
59282
  });
58909
59283
  var DoorbellPressEventSchema = object({
58910
59284
  deviceId: number(),
@@ -58969,17 +59343,7 @@ onPressed: {
58969
59343
  history: "none",
58970
59344
  customize: doorbellCustomize
58971
59345
  };
58972
- /**
58973
- * Enum-state sensor — a string value picked from a finite option set.
58974
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58975
- * states, weather conditions, contact-source channels, mode strings).
58976
- * The option set is stable across observations; it is persisted in the
58977
- * device config blob (via the control slice at adoption) and is NOT
58978
- * stored in `sourceInfo`.
58979
- */
58980
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58981
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58982
- var EnumSensorDateTimeFormatSchema = _enum([
59346
+ _enum([
58983
59347
  "date",
58984
59348
  "time",
58985
59349
  "datetime"
@@ -58987,12 +59351,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58987
59351
  var EnumSensorStatusSchema = object({
58988
59352
  value: string(),
58989
59353
  /**
58990
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58991
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58992
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58993
- * a time-only sensor → `'time'`.
59354
+ * Set when `value` is a timestamp rather than an enum state, so history
59355
+ * does not learn ISO strings as states. The display shape (date / time /
59356
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58994
59357
  */
58995
- format: EnumSensorDateTimeFormatSchema.optional(),
59358
+ timestamp: boolean().optional(),
58996
59359
  /** Ms epoch when the slice was last updated. */
58997
59360
  lastFetchedAt: number()
58998
59361
  });
@@ -59019,11 +59382,11 @@ var enumSensorCapability = {
59019
59382
  * History (D850): the value as an `enum` series (an appliance's program or
59020
59383
  * operation state, an HVAC action). The option set is per device, so the
59021
59384
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59022
- * `format` carries a timestamp, not a state, and is not recorded.
59385
+ * `timestamp` carries a clock, not a state, and is not recorded.
59023
59386
  */
59024
59387
  history: { fields: { value: {
59025
59388
  kind: "enum",
59026
- skipWhenSet: "format"
59389
+ skipWhenSet: "timestamp"
59027
59390
  } } },
59028
59391
  /** Clock fields: written, but excluded from the compare that decides
59029
59392
  * whether persisting is worth a SQLite commit. */
@@ -59043,13 +59406,13 @@ var EventFireSchema = object({
59043
59406
  deviceId: number(),
59044
59407
  eventType: string(),
59045
59408
  data: record(string(), unknown()).nullable(),
59046
- timestamp: number(),
59047
- seq: number()
59409
+ timestamp: number().meta(readingMeta("none")),
59410
+ seq: number().meta(readingMeta("count"))
59048
59411
  });
59049
59412
  var EventEmitterStatusSchema = object({
59050
59413
  eventTypes: array(string()),
59051
59414
  lastEvent: EventFireSchema.nullable(),
59052
- eventCountSinceStart: number()
59415
+ eventCountSinceStart: number().meta(readingMeta("count"))
59053
59416
  });
59054
59417
  var eventEmitterCapability = {
59055
59418
  name: "event-emitter",
@@ -59462,10 +59825,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59462
59825
  var FanControlStatusSchema = object({
59463
59826
  /** Active speed as 0..100 inclusive. Null when the device has no
59464
59827
  * speed surface (single-speed fan). */
59465
- percentage: number().min(0).max(100).nullable(),
59828
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59466
59829
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59467
59830
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59468
- percentageStep: number().positive().optional(),
59831
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59469
59832
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59470
59833
  * — empty string when no preset is active or supported. */
59471
59834
  preset: string(),
@@ -59586,7 +59949,7 @@ var FeatureProbeStatusSchema = object({
59586
59949
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59587
59950
  model: string().nullable(),
59588
59951
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59589
- channelCount: number().nullable(),
59952
+ channelCount: number().nullable().meta(readingMeta("count")),
59590
59953
  /**
59591
59954
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59592
59955
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59709,9 +60072,91 @@ var gasCapability = {
59709
60072
  * whether persisting is worth a SQLite commit. */
59710
60073
  volatileStateFields: ["lastChangedAt"]
59711
60074
  };
60075
+ var GroupCommandsSchema = object({
60076
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60077
+ results: array(object({
60078
+ deviceId: number().int().nonnegative(),
60079
+ cap: string().min(1),
60080
+ method: string().min(1),
60081
+ ok: boolean(),
60082
+ error: string().optional(),
60083
+ /** Ms epoch of the attempt. */
60084
+ at: number()
60085
+ })) });
60086
+ var groupCommandsCapability = {
60087
+ name: "group-commands",
60088
+ scope: "device",
60089
+ deviceNative: true,
60090
+ mode: "singleton",
60091
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60092
+ methods: {},
60093
+ status: {
60094
+ schema: GroupCommandsSchema,
60095
+ kind: "push"
60096
+ },
60097
+ runtimeState: GroupCommandsSchema,
60098
+ /**
60099
+ * Runtime-state durability: **session** — the last attempt. A restored
60100
+ * failure would outlive the command that caused it.
60101
+ *
60102
+ * See `RuntimeStateDurability`. Enforced by
60103
+ * `scripts/check-runtime-state-durability.ts`.
60104
+ */
60105
+ durability: "session",
60106
+ history: "none"
60107
+ };
60108
+ /**
60109
+ * How many members of a helper group answered (D944). Counts are integers,
60110
+ * not measurements: zero included is a real count, never a stand-in reading.
60111
+ * The reading itself stays on the output capability. History is none — the
60112
+ * series that later statistics read is the output cap's own history block.
60113
+ */
60114
+ var GroupStatusSchema = object({
60115
+ /** Members whose current value entered the aggregate. */
60116
+ included: number().int().nonnegative().meta(readingMeta("count", {
60117
+ precision: 0,
60118
+ label: "Reporting"
60119
+ })),
60120
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60121
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60122
+ precision: 0,
60123
+ label: "Excluded"
60124
+ })),
60125
+ /** `included` + `excluded`. */
60126
+ total: number().int().nonnegative().meta(readingMeta("count", {
60127
+ precision: 0,
60128
+ label: "Members"
60129
+ }))
60130
+ });
60131
+ var groupStatusCapability = {
60132
+ name: "group-status",
60133
+ scope: "device",
60134
+ deviceNative: true,
60135
+ mode: "singleton",
60136
+ deviceTypes: [
60137
+ DeviceType.Sensor,
60138
+ DeviceType.Switch,
60139
+ DeviceType.Light
60140
+ ],
60141
+ methods: {},
60142
+ status: {
60143
+ schema: GroupStatusSchema,
60144
+ kind: "push"
60145
+ },
60146
+ runtimeState: GroupStatusSchema,
60147
+ /**
60148
+ * Runtime-state durability: **session** — recomputed from members on the
60149
+ * next source event; a restored count would outlive the readings it named.
60150
+ *
60151
+ * See `RuntimeStateDurability`. Enforced by
60152
+ * `scripts/check-runtime-state-durability.ts`.
60153
+ */
60154
+ durability: "session",
60155
+ history: "none"
60156
+ };
59712
60157
  var HeatingValveStatusSchema = object({
59713
60158
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59714
- position: number().min(0).max(100).nullable(),
60159
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59715
60160
  /** Ms epoch when the slice was last updated (push or poll). */
59716
60161
  lastFetchedAt: number()
59717
60162
  });
@@ -59754,9 +60199,9 @@ var HumidifierStatusSchema = object({
59754
60199
  /** Whether the humidifier is currently on. */
59755
60200
  on: boolean(),
59756
60201
  /** Current measured relative humidity (0..100). Null when not reported. */
59757
- currentHumidity: number().min(0).max(100).nullable(),
60202
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59758
60203
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59759
- targetHumidity: number().min(0).max(100).nullable(),
60204
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59760
60205
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59761
60206
  * device has no mode surface. */
59762
60207
  mode: string().nullable(),
@@ -59766,9 +60211,9 @@ var HumidifierStatusSchema = object({
59766
60211
  * `idle` / `off`). Null when the device doesn't report it. */
59767
60212
  action: string().nullable(),
59768
60213
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59769
- minHumidity: number().nullable(),
60214
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59770
60215
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59771
- maxHumidity: number().nullable(),
60216
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59772
60217
  /** Ms epoch when the slice was last updated. */
59773
60218
  lastChangedAt: number()
59774
60219
  });
@@ -59838,22 +60283,13 @@ var humidifierCapability = {
59838
60283
  * `sourceInfo`.
59839
60284
  */
59840
60285
  var HumiditySensorStatusSchema = object({
59841
- /** Current relative humidity, 0..100. */
59842
- percent: number().min(0).max(100).meta({
59843
- unit: "%",
60286
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60287
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60288
+ precision: 0,
59844
60289
  label: "Humidity"
59845
- }),
60290
+ })),
59846
60291
  /** Ms epoch when the slice was last updated. */
59847
- lastFetchedAt: number(),
59848
- /** Live display unit from the upstream source (e.g. HA
59849
- * `attributes.unit_of_measurement`). The UI prefers this over the
59850
- * role's canonical unit. Absent → fall back to the canonical unit. */
59851
- unit: string().optional(),
59852
- /** Suggested decimal places for numeric display.
59853
- * Populated live from the upstream source when provided (e.g. HA
59854
- * `attributes.suggested_display_precision`). Falls back to
59855
- * auto-formatting when absent. */
59856
- precision: number().int().min(0).max(10).optional()
60292
+ lastFetchedAt: number()
59857
60293
  });
59858
60294
  var humiditySensorCapability = {
59859
60295
  name: "humidity-sensor",
@@ -59884,12 +60320,7 @@ var humiditySensorCapability = {
59884
60320
  } } },
59885
60321
  /** Clock fields: written, but excluded from the compare that decides
59886
60322
  * whether persisting is worth a SQLite commit. */
59887
- volatileStateFields: ["lastFetchedAt"],
59888
- customize: {
59889
- version: 1,
59890
- advanced: "derive",
59891
- targets: []
59892
- }
60323
+ volatileStateFields: ["lastFetchedAt"]
59893
60324
  };
59894
60325
  /**
59895
60326
  * Image display cap. Models a single still image exposed by an integration —
@@ -59910,7 +60341,7 @@ var ImageStatusSchema = object({
59910
60341
  * entity exposes no `entity_picture` (yet). */
59911
60342
  url: string().nullable(),
59912
60343
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59913
- lastUpdated: number().nullable()
60344
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59914
60345
  });
59915
60346
  var imageCapability = {
59916
60347
  name: "image",
@@ -59998,19 +60429,19 @@ var NormalizedRangeSchema = object({
59998
60429
  */
59999
60430
  var ImageSettingsStatusSchema = object({
60000
60431
  /** Normalized 0–100. */
60001
- brightness: number().optional(),
60432
+ brightness: number().optional().meta(readingMeta("none")),
60002
60433
  /** Normalized 0–100. */
60003
- contrast: number().optional(),
60434
+ contrast: number().optional().meta(readingMeta("none")),
60004
60435
  /** Normalized 0–100. */
60005
- saturation: number().optional(),
60436
+ saturation: number().optional().meta(readingMeta("none")),
60006
60437
  /** Normalized 0–100. */
60007
- sharpness: number().optional(),
60438
+ sharpness: number().optional().meta(readingMeta("none")),
60008
60439
  mirror: boolean().optional(),
60009
60440
  flip: boolean().optional(),
60010
60441
  rotate: ImageRotateSchema.optional(),
60011
60442
  whiteBalance: WhiteBalanceModeSchema.optional(),
60012
60443
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60013
- warmth: number().optional(),
60444
+ warmth: number().optional().meta(readingMeta("none")),
60014
60445
  exposureMode: ExposureModeSchema.optional(),
60015
60446
  backlightMode: BacklightModeSchema.optional(),
60016
60447
  lastFetchedAt: number()
@@ -60289,10 +60720,10 @@ var IntercomAbilitySchema = object({
60289
60720
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60290
60721
  codecs: array(string()),
60291
60722
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60292
- sampleRate: number(),
60723
+ sampleRate: number().meta(readingMeta("none")),
60293
60724
  duplex: _enum(["half", "full"]),
60294
60725
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60295
- maxBacklogMs: number()
60726
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60296
60727
  });
60297
60728
  var IntercomStatusSchema = object({
60298
60729
  /** True while a talk session is open. */
@@ -60519,13 +60950,13 @@ var LawnMowerControlStatusSchema = object({
60519
60950
  activity: LawnMowerActivitySchema,
60520
60951
  /** 0..100 battery percentage. Null when the device has no battery
60521
60952
  * reading. */
60522
- batteryLevel: number().min(0).max(100).nullable(),
60953
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60523
60954
  /** 0..100 mowing-completion percentage of the current task, or null when no
60524
60955
  * task is active / progress is unavailable. */
60525
- progressPercent: number().min(0).max(100).nullable(),
60956
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60526
60957
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60527
60958
  * or null when unknown. */
60528
- currentCode: number().nullable(),
60959
+ currentCode: number().nullable().meta(readingMeta("none")),
60529
60960
  /** Human label for {@link currentCode}, or null when undecodable. */
60530
60961
  currentCodeLabel: string().nullable(),
60531
60962
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61058,7 +61489,7 @@ var MediaPlayerStatusSchema = object({
61058
61489
  state: MediaPlayerStateSchema,
61059
61490
  /** Volume as 0..100 inclusive. Null when the device has no volume
61060
61491
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61061
- volumeLevel: number().min(0).max(100).nullable(),
61492
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61062
61493
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61063
61494
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61064
61495
  isMuted: boolean().nullable(),
@@ -61071,10 +61502,10 @@ var MediaPlayerStatusSchema = object({
61071
61502
  currentMedia: MediaInfoSchema.nullable(),
61072
61503
  /** Current playback position in ms. Null when not seekable or
61073
61504
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61074
- positionMs: number().int().nonnegative().nullable(),
61505
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61075
61506
  /** Total duration of the current media in ms. Null when unknown
61076
61507
  * (live stream) or nothing is playing. */
61077
- durationMs: number().int().nonnegative().nullable(),
61508
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61078
61509
  /** Shuffle toggle. Null when no shuffle surface.
61079
61510
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61080
61511
  shuffle: boolean().nullable(),
@@ -61383,7 +61814,7 @@ var motionCustomize = {
61383
61814
  eventName: "onMotionChanged"
61384
61815
  },
61385
61816
  deviceTypes: [DeviceType.Camera],
61386
- apply: "replace",
61817
+ apply: "add",
61387
61818
  applyAuthority: {
61388
61819
  kind: "camera-sensors",
61389
61820
  timeoutField: "motionCooldownMs"
@@ -61455,7 +61886,7 @@ var MotionStatusSchema = object({
61455
61886
  * it, every provider seeded it at boot, and every session left it
61456
61887
  * `detected: true` after its detach.
61457
61888
  */
61458
- autoClearAfterMs: number().nullable()
61889
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61459
61890
  });
61460
61891
  /**
61461
61892
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61492,12 +61923,18 @@ var motionCapability = {
61492
61923
  methods: {
61493
61924
  /**
61494
61925
  * Pull the current motion state synchronously. Convenience shortcut
61495
- * for consumers that don't need the full status object; equivalent
61496
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61497
- * `getStatus` once the auto-injected status surface lands in every
61498
- * consumer.
61926
+ * for consumers that don't need the full status object. A composed
61927
+ * group answers from its slice (D944): unknown throws, it is not off.
61499
61928
  */
61500
61929
  isDetected: method(object({ deviceId: number() }), boolean()) },
61930
+ /**
61931
+ * A composed motion group serves `isDetected` from `detected` (D944).
61932
+ * Without this the cap stays uncomposable (`has-commands`).
61933
+ */
61934
+ composedMethods: { isDetected: {
61935
+ kind: "slice",
61936
+ field: "detected"
61937
+ } },
61501
61938
  events: {
61502
61939
  /**
61503
61940
  * Fires every time the runner transitions a camera between
@@ -61637,7 +62074,7 @@ var MotionZoneRegionSchema = object({
61637
62074
  * the grid region(s). */
61638
62075
  var MotionZoneStatusSchema = object({
61639
62076
  enabled: boolean(),
61640
- sensitivity: number(),
62077
+ sensitivity: number().meta(readingMeta("none")),
61641
62078
  /** Grid region(s). Today exactly one `grid` shape. */
61642
62079
  regions: array(MotionZoneRegionSchema),
61643
62080
  lastFetchedAt: number()
@@ -62007,7 +62444,7 @@ var NavigationStatusSchema = object({
62007
62444
  following: boolean(),
62008
62445
  flash: boolean(),
62009
62446
  lightMode: NavigationLightModeSchema,
62010
- lightLevel: number().min(40).max(100),
62447
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62011
62448
  /** Ms epoch when the slice was last updated. */
62012
62449
  lastChangedAt: number()
62013
62450
  });
@@ -62141,13 +62578,13 @@ var NetworkLinkStatusSchema = object({
62141
62578
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62142
62579
  * SKIP a null rather than coerce it.
62143
62580
  */
62144
- signalPercent: number().min(0).max(100).nullable(),
62581
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62145
62582
  /** Raw received signal strength in dBm, when the firmware reports one. */
62146
- rssiDbm: number().optional(),
62583
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62147
62584
  /** Network name of a wireless link, when the firmware reports it. */
62148
62585
  ssid: string().optional(),
62149
62586
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62150
- lastUpdated: number()
62587
+ lastUpdated: number().meta(readingMeta("none"))
62151
62588
  });
62152
62589
  var networkLinkCapability = {
62153
62590
  name: "network-link",
@@ -62532,7 +62969,7 @@ var NotifierStatusSchema = object({
62532
62969
  lastError: string().nullable(),
62533
62970
  /** Number of deliveries currently buffered server-side (rate-limited
62534
62971
  * notifiers may queue). 0 when the provider sends synchronously. */
62535
- queueDepth: number().int().nonnegative(),
62972
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62536
62973
  /** Per-feature capability matrix — authoritative source for the
62537
62974
  * compose-form field gating. */
62538
62975
  supports: NotifierSupportsSchema
@@ -62624,11 +63061,9 @@ onSent: { data: object({
62624
63061
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62625
63062
  * for: water flow, distance, weight, frequency, signal strength, …).
62626
63063
  *
62627
- * The `unit` and `precision` fields carry the live display metadata that
62628
- * the upstream source provides with each state push (HA
62629
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62630
- * The UI reads them directly from the slice — they are the single source
62631
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63064
+ * The quantity is not on this slice. It is chosen when the device is
63065
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63066
+ * that quantity's base unit. `null` is a refused sample (D393).
62632
63067
  *
62633
63068
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62634
63069
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62637,15 +63072,8 @@ onSent: { data: object({
62637
63072
  * numeric values land here.
62638
63073
  */
62639
63074
  var NumericSensorStatusSchema = object({
62640
- value: number(),
62641
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62642
- * Populated live from the upstream source on each state push.
62643
- * Absent for unitless sensors. */
62644
- unit: string().optional(),
62645
- /** Suggested decimal places for numeric display.
62646
- * Populated live from the upstream source when provided.
62647
- * Falls back to auto-formatting when absent. */
62648
- precision: number().int().min(0).max(10).optional(),
63075
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63076
+ value: number().nullable().meta(readingMeta("device")),
62649
63077
  /** Ms epoch when the slice was last updated. */
62650
63078
  lastFetchedAt: number()
62651
63079
  });
@@ -62669,11 +63097,11 @@ var numericSensorCapability = {
62669
63097
  */
62670
63098
  durability: "restored",
62671
63099
  /**
62672
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63100
+ * History (D850): recorded series. The unit is the device quantity's base,
63101
+ * resolved when the series is read — the cap has no single unit.
62673
63102
  */
62674
63103
  history: { fields: { value: {
62675
63104
  kind: "gauge",
62676
- unitFrom: "unit",
62677
63105
  minIntervalMs: 3e4
62678
63106
  } } },
62679
63107
  /** Clock fields: written, but excluded from the compare that decides
@@ -62965,23 +63393,26 @@ var PetFeederStatusSchema = object({
62965
63393
  /** Food currently in the bowl (grams). Null when the device has not
62966
63394
  * reported a reading yet. On dual-hopper models this is the combined
62967
63395
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62968
- foodLevel: number().nullable(),
63396
+ foodLevel: number().nullable().meta(readingMeta("none")),
62969
63397
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62970
63398
  * single-hopper models. */
62971
- food1: number().nullable(),
63399
+ food1: number().nullable().meta(readingMeta("none")),
62972
63400
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62973
63401
  * single-hopper models. */
62974
- food2: number().nullable(),
63402
+ food2: number().nullable().meta(readingMeta("none")),
62975
63403
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62976
63404
  * (`device_class: problem`, on = low). True when the bowl is empty /
62977
63405
  * below the feeder's low threshold. */
62978
63406
  lowFood: boolean(),
62979
63407
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62980
63408
  * device has no battery reading. */
62981
- batteryPower: number().min(0).max(100).nullable(),
62982
- /** Days of desiccant life remaining. Null when the model has no
62983
- * desiccant sensor. */
62984
- desiccantLeftDays: number().nullable(),
63409
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63410
+ /**
63411
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63412
+ * Providers convert from the days the feeder reports. Null when the
63413
+ * model has no desiccant sensor.
63414
+ */
63415
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
62985
63416
  /** True while a feed is in progress. */
62986
63417
  feeding: boolean(),
62987
63418
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62992,7 +63423,7 @@ var PetFeederStatusSchema = object({
62992
63423
  * with `errorCode` for consumers that want the raw integer. */
62993
63424
  error: string().nullable(),
62994
63425
  /** Raw device error code (0 / null = no error). */
62995
- errorCode: number().nullable(),
63426
+ errorCode: number().nullable().meta(readingMeta("none")),
62996
63427
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62997
63428
  isDualHopper: boolean(),
62998
63429
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63002,7 +63433,7 @@ var PetFeederStatusSchema = object({
63002
63433
  /** Play a chime when dispensing. */
63003
63434
  feedSound: boolean(),
63004
63435
  /** Speaker / prompt volume level (device-scaled integer). */
63005
- volume: number(),
63436
+ volume: number().meta(readingMeta("none")),
63006
63437
  /** Ms epoch when the slice was last refreshed from the cloud. */
63007
63438
  lastFetchedAt: number()
63008
63439
  });
@@ -63121,7 +63552,7 @@ var petFeederCapability = {
63121
63552
  },
63122
63553
  desiccantLeftDays: {
63123
63554
  kind: "gauge",
63124
- unit: "days"
63555
+ unit: "s"
63125
63556
  }
63126
63557
  } }
63127
63558
  };
@@ -63522,36 +63953,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63522
63953
  */
63523
63954
  var PowerMeterStatusSchema = object({
63524
63955
  /** Instantaneous power draw in watts. */
63525
- watts: number().optional().meta({
63526
- unit: "W",
63527
- label: "Power"
63528
- }),
63956
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63529
63957
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63530
- kwhTotal: number().optional().meta({
63531
- unit: "kWh",
63532
- label: "Energy"
63533
- }),
63958
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63534
63959
  /** Voltage in volts. */
63535
- volts: number().optional(),
63960
+ volts: number().optional().meta(readingMeta("voltage")),
63536
63961
  /** Current in amperes. */
63537
- amps: number().optional(),
63538
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63539
- va: number().optional(),
63962
+ amps: number().optional().meta(readingMeta("current")),
63963
+ /** Apparent power in volt-amperes. */
63964
+ va: number().optional().meta(readingMeta("apparent-power")),
63540
63965
  /** Power factor (0..1) if reported. */
63541
- powerFactor: number().min(0).max(1).optional(),
63966
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63542
63967
  /** Ms epoch when the slice was last updated. */
63543
- lastFetchedAt: number(),
63544
- /** Live display unit of the single metric this slice carries (e.g. HA
63545
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63546
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63547
- * unit per slice is unambiguous. The UI prefers this over the role's
63548
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63549
- unit: string().optional(),
63550
- /** Suggested decimal places for numeric display.
63551
- * Populated live from the upstream source when provided (e.g. HA
63552
- * `attributes.suggested_display_precision`). Falls back to
63553
- * auto-formatting when absent. */
63554
- precision: number().int().min(0).max(10).optional()
63968
+ lastFetchedAt: number()
63555
63969
  });
63556
63970
  var powerMeterCapability = {
63557
63971
  name: "power-meter",
@@ -63615,12 +64029,7 @@ var powerMeterCapability = {
63615
64029
  } },
63616
64030
  /** Clock fields: written, but excluded from the compare that decides
63617
64031
  * whether persisting is worth a SQLite commit. */
63618
- volatileStateFields: ["lastFetchedAt"],
63619
- customize: {
63620
- version: 1,
63621
- advanced: "derive",
63622
- targets: []
63623
- }
64032
+ volatileStateFields: ["lastFetchedAt"]
63624
64033
  };
63625
64034
  /**
63626
64035
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63640,11 +64049,11 @@ var powerMeterCapability = {
63640
64049
  */
63641
64050
  var GpsLocationSchema = object({
63642
64051
  /** Latitude in decimal degrees, -90..90. */
63643
- latitude: number().min(-90).max(90),
64052
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63644
64053
  /** Longitude in decimal degrees, -180..180. */
63645
- longitude: number().min(-180).max(180),
64054
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63646
64055
  /** Reported accuracy in meters (lower = better). */
63647
- accuracyMeters: number().nonnegative()
64056
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63648
64057
  });
63649
64058
  var PresenceStatusSchema = object({
63650
64059
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63656,7 +64065,7 @@ var PresenceStatusSchema = object({
63656
64065
  gps: GpsLocationSchema.nullable(),
63657
64066
  /** Optional battery level of the tracking device (0..100). Null
63658
64067
  * when not reported. */
63659
- batteryPercent: number().min(0).max(100).nullable(),
64068
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63660
64069
  /** Ms epoch when the slice was last updated. */
63661
64070
  lastChangedAt: number()
63662
64071
  });
@@ -63706,19 +64115,10 @@ var presenceCapability = {
63706
64115
  * `sourceInfo`.
63707
64116
  */
63708
64117
  var PressureSensorStatusSchema = object({
63709
- /** Current pressure in hPa. */
63710
- hpa: number(),
64118
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64119
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63711
64120
  /** Ms epoch when the slice was last updated. */
63712
- lastFetchedAt: number(),
63713
- /** Live display unit from the upstream source (e.g. HA
63714
- * `attributes.unit_of_measurement`). The UI prefers this over the
63715
- * role's canonical unit. Absent → fall back to the canonical unit. */
63716
- unit: string().optional(),
63717
- /** Suggested decimal places for numeric display.
63718
- * Populated live from the upstream source when provided (e.g. HA
63719
- * `attributes.suggested_display_precision`). Falls back to
63720
- * auto-formatting when absent. */
63721
- precision: number().int().min(0).max(10).optional()
64121
+ lastFetchedAt: number()
63722
64122
  });
63723
64123
  var pressureSensorCapability = {
63724
64124
  name: "pressure-sensor",
@@ -64132,8 +64532,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64132
64532
  }), _void(), { kind: "mutation" });
64133
64533
  var PtzAutotrackSettingsSchema = object({
64134
64534
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64135
- stopDelaySeconds: number().int().min(0).max(300),
64136
- disappearDelaySeconds: number().int().min(0).max(300)
64535
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64536
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64137
64537
  });
64138
64538
  /**
64139
64539
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64287,7 +64687,7 @@ var RunningStateStatusSchema = object({
64287
64687
  /** null = unknown — never coerced to false (D393). */
64288
64688
  running: boolean().nullable(),
64289
64689
  /** Epoch ms the current boolean began; null while `running` is null. */
64290
- since: number().nullable(),
64690
+ since: number().nullable().meta(readingMeta("none")),
64291
64691
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64292
64692
  phase: string().nullable(),
64293
64693
  /** null until a reading has been published (D393). */
@@ -64302,7 +64702,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64302
64702
  /** Generic Customize target for the running boolean (D936). */
64303
64703
  var runningCustomize = {
64304
64704
  version: 1,
64305
- advanced: "derive",
64306
64705
  targets: [{
64307
64706
  id: "running",
64308
64707
  cardinality: "scalar",
@@ -64322,7 +64721,7 @@ var runningCustomize = {
64322
64721
  DeviceType.Thermostat,
64323
64722
  DeviceType.PetFeeder
64324
64723
  ],
64325
- apply: "add-or-replace",
64724
+ apply: "add",
64326
64725
  path: "running",
64327
64726
  sources: [
64328
64727
  {
@@ -65586,9 +65985,9 @@ var RecordTriggerSchema = _enum([
65586
65985
  */
65587
65986
  var RecordWindowSchema = object({
65588
65987
  trigger: RecordTriggerSchema,
65589
- day: number().int().min(0).max(6),
65590
- startMinute: number().int().min(0).max(1439),
65591
- endMinute: number().int().min(1).max(1440)
65988
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65989
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65990
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65592
65991
  });
65593
65992
  /** Status of one physical volume, as the camera itself describes it. */
65594
65993
  var OnboardStorageVolumeSchema = object({
@@ -65664,10 +66063,10 @@ var RecordingOnboardStatusSchema = object({
65664
66063
  /** Master "record to the card at all" switch. */
65665
66064
  enabled: boolean().nullable(),
65666
66065
  overwriteWhenFull: boolean().nullable(),
65667
- preRecordSec: number().nullable(),
65668
- postRecordSec: number().nullable(),
66066
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66067
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65669
66068
  /** Length of one recorded file, in minutes. */
65670
- segmentMinutes: number().nullable(),
66069
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65671
66070
  /** The primary track's weekly windows, flattened. */
65672
66071
  windows: array(RecordWindowSchema),
65673
66072
  /**
@@ -65683,7 +66082,7 @@ var RecordingOnboardStatusSchema = object({
65683
66082
  * provider reporting a non-zero count also reports the schedule as not
65684
66083
  * writable.
65685
66084
  */
65686
- unreadableWindows: number(),
66085
+ unreadableWindows: number().meta(readingMeta("count")),
65687
66086
  /**
65688
66087
  * The camera is scheduled to record and has NO usable storage.
65689
66088
  *
@@ -66528,7 +66927,7 @@ var CamStreamDescriptorSchema = object({
66528
66927
  url: string().optional(),
66529
66928
  codec: string().optional(),
66530
66929
  resolution: CamStreamResolutionSchema.optional(),
66531
- fps: number().positive().optional(),
66930
+ fps: number().positive().optional().meta(readingMeta("rate")),
66532
66931
  label: string().optional(),
66533
66932
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66534
66933
  deviceFeatures: array(string()).optional(),
@@ -66615,18 +67014,18 @@ var StreamProfileSchema = _enum([
66615
67014
  ]);
66616
67015
  /** The current encoding config of one stream profile. */
66617
67016
  var StreamProfileConfigSchema = object({
66618
- width: number(),
66619
- height: number(),
67017
+ width: number().meta(readingMeta("none")),
67018
+ height: number().meta(readingMeta("none")),
66620
67019
  codec: _enum(["h264", "h265"]),
66621
- framerate: number(),
66622
- bitrate: number(),
67020
+ framerate: number().meta(readingMeta("rate")),
67021
+ bitrate: number().meta(readingMeta("bitrate")),
66623
67022
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66624
67023
  encoderProfile: _enum([
66625
67024
  "high",
66626
67025
  "main",
66627
67026
  "baseline"
66628
67027
  ]).optional(),
66629
- gop: number().optional(),
67028
+ gop: number().optional().meta(readingMeta("none")),
66630
67029
  /**
66631
67030
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66632
67031
  *
@@ -66847,12 +67246,7 @@ var switchCapability = {
66847
67246
  label: "On",
66848
67247
  kind: "boolean"
66849
67248
  }
66850
- }] },
66851
- customize: {
66852
- version: 1,
66853
- advanced: "derive",
66854
- targets: []
66855
- }
67249
+ }] }
66856
67250
  };
66857
67251
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66858
67252
  function hasControlCharacter(value) {
@@ -67435,35 +67829,21 @@ var tamperCapability = {
67435
67829
  * entries with `device_class: temperature` and any future native
67436
67830
  * thermometer.
67437
67831
  *
67438
- * Unit normalisation: providers convert to Celsius before storing.
67439
- * The slice value is always Celsius so cross-cap aggregators
67440
- * (climate-control's `currentTemp`, energy analytics) can compose
67441
- * without per-source unit fixups. The canonical display unit (`°C`) is
67442
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67443
- * `sourceInfo`.
67832
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67833
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67444
67834
  *
67445
67835
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67446
67836
  * frozen feed (provider hung) distinct from a "temperature hasn't
67447
67837
  * changed" steady state.
67448
67838
  */
67449
67839
  var TemperatureSensorStatusSchema = object({
67450
- /** Current temperature in Celsius. */
67451
- celsius: number().meta({
67452
- unit: "°C",
67840
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67841
+ celsius: number().nullable().meta(readingMeta("temperature", {
67842
+ precision: 1,
67453
67843
  label: "Temperature"
67454
- }),
67844
+ })),
67455
67845
  /** Ms epoch when the slice was last updated (push or poll). */
67456
- lastFetchedAt: number(),
67457
- /** Live display unit from the upstream source (e.g. HA
67458
- * `attributes.unit_of_measurement`). The UI prefers this over the
67459
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67460
- * Absent → the UI falls back to the role's canonical unit. */
67461
- unit: string().optional(),
67462
- /** Suggested decimal places for numeric display.
67463
- * Populated live from the upstream source when provided (e.g. HA
67464
- * `attributes.suggested_display_precision`). Falls back to
67465
- * auto-formatting when absent. */
67466
- precision: number().int().min(0).max(10).optional()
67846
+ lastFetchedAt: number()
67467
67847
  });
67468
67848
  var temperatureSensorCapability = {
67469
67849
  name: "temperature-sensor",
@@ -67494,12 +67874,7 @@ var temperatureSensorCapability = {
67494
67874
  } } },
67495
67875
  /** Clock fields: written, but excluded from the compare that decides
67496
67876
  * whether persisting is worth a SQLite commit. */
67497
- volatileStateFields: ["lastFetchedAt"],
67498
- customize: {
67499
- version: 1,
67500
- advanced: "derive",
67501
- targets: []
67502
- }
67877
+ volatileStateFields: ["lastFetchedAt"]
67503
67878
  };
67504
67879
  /**
67505
67880
  * toast — system-scoped singleton capability that streams toast
@@ -67782,16 +68157,16 @@ var VacuumControlStatusSchema = object({
67782
68157
  ]),
67783
68158
  /** 0..100 battery percentage. Null when the device has no battery
67784
68159
  * reading. */
67785
- batteryLevel: number().min(0).max(100).nullable(),
68160
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67786
68161
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67787
68162
  * the vacuum has no speed control. */
67788
68163
  fanSpeed: string().nullable(),
67789
68164
  /** Speed tokens the hardware accepts — drives the UI selector. */
67790
68165
  availableFanSpeeds: array(string()),
67791
68166
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67792
- progressPercent: number().min(0).max(100).nullable(),
68167
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67793
68168
  /** Current error code (0 / null = no error). */
67794
- errorCode: number().nullable(),
68169
+ errorCode: number().nullable().meta(readingMeta("none")),
67795
68170
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67796
68171
  errorLabel: string().nullable(),
67797
68172
  /** Ms epoch when the slice was last updated. */
@@ -67869,7 +68244,7 @@ var ValveStatusSchema = object({
67869
68244
  ]),
67870
68245
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67871
68246
  * intermediate position surface. */
67872
- position: number().min(0).max(100).nullable(),
68247
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67873
68248
  /** Ms epoch when the slice was last updated. */
67874
68249
  lastChangedAt: number()
67875
68250
  });
@@ -67977,9 +68352,9 @@ var vibrationCapability = {
67977
68352
  */
67978
68353
  var WaterHeaterStatusSchema = object({
67979
68354
  /** Current measured temperature. Null when not reported. */
67980
- currentTemp: number().nullable(),
68355
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67981
68356
  /** Target temperature setpoint. Null when no setpoint surface. */
67982
- targetTemp: number().nullable(),
68357
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67983
68358
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67984
68359
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67985
68360
  * device reports an unknown state. */
@@ -67990,9 +68365,9 @@ var WaterHeaterStatusSchema = object({
67990
68365
  * has no away surface. */
67991
68366
  away: boolean().nullable(),
67992
68367
  /** HA `min_temp` attribute. Null when not reported. */
67993
- minTemp: number().nullable(),
68368
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67994
68369
  /** HA `max_temp` attribute. Null when not reported. */
67995
- maxTemp: number().nullable(),
68370
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67996
68371
  /** Ms epoch when the slice was last updated. */
67997
68372
  lastChangedAt: number()
67998
68373
  });
@@ -68077,25 +68452,18 @@ var waterHeaterCapability = {
68077
68452
  * Forecast deferred — current conditions only for v1.
68078
68453
  */
68079
68454
  var WeatherStatusSchema = object({
68080
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68081
- * when no condition has been reported yet. */
68455
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68082
68456
  condition: string().nullable(),
68083
- /** Current temperature in the reported unit. Null when not provided. */
68084
- temperature: number().nullable(),
68085
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68086
- temperatureUnit: string().nullable(),
68457
+ /** Current temperature in Celsius. Null when not provided or refused. */
68458
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68087
68459
  /** Relative humidity (0..100). Null when not provided. */
68088
- humidity: number().min(0).max(100).nullable(),
68089
- /** Barometric pressure in the reported unit. Null when not provided. */
68090
- pressure: number().nullable(),
68091
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68092
- pressureUnit: string().nullable(),
68093
- /** Wind speed in the reported unit. Null when not provided. */
68094
- windSpeed: number().nullable(),
68095
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68096
- windSpeedUnit: string().nullable(),
68460
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68461
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68462
+ pressure: number().nullable().meta(readingMeta("pressure")),
68463
+ /** Wind speed in metres per second. Null when not provided or refused. */
68464
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68097
68465
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68098
- windBearing: number().nullable(),
68466
+ windBearing: number().nullable().meta(readingMeta("angle")),
68099
68467
  /** Ms epoch when the slice was last updated. */
68100
68468
  lastFetchedAt: number()
68101
68469
  });
@@ -68123,12 +68491,12 @@ var weatherCapability = {
68123
68491
  */
68124
68492
  durability: "session",
68125
68493
  /**
68126
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68494
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68127
68495
  */
68128
68496
  history: { fields: {
68129
68497
  temperature: {
68130
68498
  kind: "gauge",
68131
- unitFrom: "temperatureUnit",
68499
+ unit: "°C",
68132
68500
  deadband: .2
68133
68501
  },
68134
68502
  humidity: {
@@ -68138,12 +68506,12 @@ var weatherCapability = {
68138
68506
  },
68139
68507
  pressure: {
68140
68508
  kind: "gauge",
68141
- unitFrom: "pressureUnit",
68509
+ unit: "hPa",
68142
68510
  deadband: .5
68143
68511
  },
68144
68512
  windSpeed: {
68145
68513
  kind: "gauge",
68146
- unitFrom: "windSpeedUnit",
68514
+ unit: "m/s",
68147
68515
  deadband: .5
68148
68516
  }
68149
68517
  } }
@@ -69374,6 +69742,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69374
69742
  featureProbe: featureProbeCapability,
69375
69743
  flood: floodCapability,
69376
69744
  gas: gasCapability,
69745
+ groupCommands: groupCommandsCapability,
69746
+ groupStatus: groupStatusCapability,
69377
69747
  heatingValve: heatingValveCapability,
69378
69748
  humidifier: humidifierCapability,
69379
69749
  humiditySensor: humiditySensorCapability,
@@ -70750,6 +71120,13 @@ Object.freeze({
70750
71120
  access: "view",
70751
71121
  role: "admin"
70752
71122
  },
71123
+ "addons.getPackageDocs": {
71124
+ capName: "addons",
71125
+ capScope: "system",
71126
+ addonId: null,
71127
+ access: "view",
71128
+ role: "admin"
71129
+ },
70753
71130
  "addons.getVersions": {
70754
71131
  capName: "addons",
70755
71132
  capScope: "system",
@@ -73858,6 +74235,20 @@ Object.freeze({
73858
74235
  access: "view",
73859
74236
  role: "viewer"
73860
74237
  },
74238
+ "groupCommands.getStatus": {
74239
+ capName: "group-commands",
74240
+ capScope: "device",
74241
+ addonId: null,
74242
+ access: "view",
74243
+ role: "viewer"
74244
+ },
74245
+ "groupStatus.getStatus": {
74246
+ capName: "group-status",
74247
+ capScope: "device",
74248
+ addonId: null,
74249
+ access: "view",
74250
+ role: "viewer"
74251
+ },
73861
74252
  "heatingValve.getStatus": {
73862
74253
  capName: "heating-valve",
73863
74254
  capScope: "device",
@@ -80332,7 +80723,7 @@ function t(en, it) {
80332
80723
  * covers, valves, locks, climate, fans, alarm panels.
80333
80724
  */
80334
80725
  var ON = t("On", "Acceso");
80335
- var PERCENT$1 = t("Percentage", "Percentuale");
80726
+ var PERCENT = t("Percentage", "Percentuale");
80336
80727
  var OPEN_CLOSE_STATES = [
80337
80728
  {
80338
80729
  value: "open",
@@ -80560,7 +80951,7 @@ var ACTUATOR_CAP_VOCABULARY = {
80560
80951
  icon: "sun-medium",
80561
80952
  openArgs: [{
80562
80953
  key: "percentage",
80563
- label: PERCENT$1,
80954
+ label: PERCENT,
80564
80955
  offer: {
80565
80956
  kind: "number",
80566
80957
  min: 0,
@@ -83680,6 +84071,37 @@ var SENSOR_CAP_VOCABULARY = {
83680
84071
  icon: "flame"
83681
84072
  }]
83682
84073
  },
84074
+ "group-commands": {
84075
+ scope: "device",
84076
+ noun: t("Group command", "Comando di gruppo")
84077
+ },
84078
+ "group-status": {
84079
+ scope: "device",
84080
+ noun: t("Group", "Gruppo"),
84081
+ state: [
84082
+ {
84083
+ kind: "numeric",
84084
+ field: "included",
84085
+ quantity: t("the members reporting", "i membri che rispondono"),
84086
+ label: t("Reporting", "Rispondono"),
84087
+ icon: "users"
84088
+ },
84089
+ {
84090
+ kind: "numeric",
84091
+ field: "excluded",
84092
+ quantity: t("the members left out", "i membri esclusi"),
84093
+ label: t("Excluded", "Esclusi"),
84094
+ icon: "users"
84095
+ },
84096
+ {
84097
+ kind: "numeric",
84098
+ field: "total",
84099
+ quantity: t("the member count", "il numero di membri"),
84100
+ label: t("Members", "Membri"),
84101
+ icon: "sigma"
84102
+ }
84103
+ ]
84104
+ },
83683
84105
  "humidity-sensor": {
83684
84106
  scope: "device",
83685
84107
  noun: t("Humidity", "Umidità"),
@@ -83713,7 +84135,6 @@ var SENSOR_CAP_VOCABULARY = {
83713
84135
  field: "value",
83714
84136
  quantity: t("the reading", "il valore"),
83715
84137
  label: t("Reading", "Valore"),
83716
- unitField: "unit",
83717
84138
  icon: "gauge"
83718
84139
  }]
83719
84140
  },
@@ -83849,7 +84270,7 @@ var SENSOR_CAP_VOCABULARY = {
83849
84270
  field: "temperature",
83850
84271
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83851
84272
  label: t("Temperature", "Temperatura"),
83852
- unitField: "temperatureUnit",
84273
+ unit: "°C",
83853
84274
  icon: "thermometer"
83854
84275
  },
83855
84276
  {
@@ -83865,7 +84286,7 @@ var SENSOR_CAP_VOCABULARY = {
83865
84286
  field: "windSpeed",
83866
84287
  quantity: t("the wind speed", "la velocità del vento"),
83867
84288
  label: t("Wind", "Vento"),
83868
- unitField: "windSpeedUnit",
84289
+ unit: "m/s",
83869
84290
  icon: "wind"
83870
84291
  },
83871
84292
  {
@@ -83873,7 +84294,7 @@ var SENSOR_CAP_VOCABULARY = {
83873
84294
  field: "pressure",
83874
84295
  quantity: t("the air pressure", "la pressione atmosferica"),
83875
84296
  label: t("Pressure", "Pressione"),
83876
- unitField: "pressureUnit",
84297
+ unit: "hPa",
83877
84298
  icon: "gauge"
83878
84299
  }
83879
84300
  ]
@@ -85146,6 +85567,16 @@ Object.freeze({
85146
85567
  form: "single",
85147
85568
  optional: false
85148
85569
  }],
85570
+ "groupCommands.getStatus": [{
85571
+ name: "deviceId",
85572
+ form: "single",
85573
+ optional: false
85574
+ }],
85575
+ "groupStatus.getStatus": [{
85576
+ name: "deviceId",
85577
+ form: "single",
85578
+ optional: false
85579
+ }],
85149
85580
  "heatingValve.getStatus": [{
85150
85581
  name: "deviceId",
85151
85582
  form: "single",
@@ -87130,6 +87561,8 @@ new Map(Object.entries({
87130
87561
  "detection-blind": "device-events",
87131
87562
  "motion-noisy": "device-events",
87132
87563
  "motion-calm": "device-events",
87564
+ "package-llm-silent": "device-events",
87565
+ "package-llm-answered": "device-events",
87133
87566
  "recording-stalled": "device-events",
87134
87567
  "recording-healthy": "device-events",
87135
87568
  "recording-degraded": "device-events",
@@ -87213,6 +87646,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87213
87646
  label: "Motion without a subject",
87214
87647
  kinds: ["motion-noisy", "motion-calm"]
87215
87648
  },
87649
+ {
87650
+ id: "package-llm",
87651
+ family: "device-events",
87652
+ label: "Package confirm",
87653
+ kinds: ["package-llm-silent", "package-llm-answered"]
87654
+ },
87216
87655
  {
87217
87656
  id: "recording",
87218
87657
  family: "device-events",
@@ -87394,6 +87833,8 @@ var DEVICE_EVENTS = [
87394
87833
  "detection-blind",
87395
87834
  "motion-noisy",
87396
87835
  "motion-calm",
87836
+ "package-llm-silent",
87837
+ "package-llm-answered",
87397
87838
  "recording-stalled",
87398
87839
  "recording-healthy",
87399
87840
  "recording-degraded",
@@ -87662,6 +88103,15 @@ new Map([
87662
88103
  snapshotLabel: "Cameras with constant motion now",
87663
88104
  snapshotSection: null
87664
88105
  },
88106
+ {
88107
+ id: "package-llm-confirm",
88108
+ family: "device-events",
88109
+ label: "Package confirm (not answering / answering again)",
88110
+ bad: ["package-llm-silent"],
88111
+ recovery: ["package-llm-answered"],
88112
+ snapshotLabel: "Cameras whose package confirm is not answering",
88113
+ snapshotSection: null
88114
+ },
87665
88115
  {
87666
88116
  id: "recording-health",
87667
88117
  family: "device-events",
@@ -88764,17 +89214,15 @@ function entityIdentity(descriptor) {
88764
89214
  function slugifyKey(value) {
88765
89215
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
88766
89216
  }
88767
- var PERCENT = "%";
88768
- var WATT = "W";
88769
89217
  /** Clamp a raw percentage into 0..100 (defensive — a hung feed can overshoot). */
88770
89218
  function pct(value) {
88771
89219
  if (!Number.isFinite(value)) return 0;
88772
89220
  return Math.max(0, Math.min(100, value));
88773
89221
  }
88774
- /** Bytes → whole GiB, rounded to 1 decimal (array/disk capacity display). */
88775
- function toGiB(bytes) {
88776
- if (!Number.isFinite(bytes) || bytes <= 0) return 0;
88777
- return Math.round(bytes / 1024 ** 3 * 10) / 10;
89222
+ /** A byte count. Negative or non-finite input is an empty reading, not a guess. */
89223
+ function bytes(value) {
89224
+ if (!Number.isFinite(value) || value < 0) return 0;
89225
+ return value;
88778
89226
  }
88779
89227
  /** Array capacity + parity + CPU + notification-count metric descriptors. */
88780
89228
  function arrayMetricDescriptors(snapshot) {
@@ -88801,8 +89249,7 @@ function arrayMetricDescriptors(snapshot) {
88801
89249
  role: DeviceRole.NumericSensor,
88802
89250
  numeric: {
88803
89251
  value: usagePercent,
88804
- unit: PERCENT,
88805
- precision: 1
89252
+ quantity: "percentage"
88806
89253
  }
88807
89254
  },
88808
89255
  {
@@ -88813,9 +89260,8 @@ function arrayMetricDescriptors(snapshot) {
88813
89260
  name: "Array total",
88814
89261
  role: DeviceRole.NumericSensor,
88815
89262
  numeric: {
88816
- value: toGiB(totalKb * 1024),
88817
- unit: "GiB",
88818
- precision: 1
89263
+ value: bytes(totalKb * 1024),
89264
+ quantity: "information"
88819
89265
  }
88820
89266
  },
88821
89267
  {
@@ -88826,9 +89272,8 @@ function arrayMetricDescriptors(snapshot) {
88826
89272
  name: "Array used",
88827
89273
  role: DeviceRole.NumericSensor,
88828
89274
  numeric: {
88829
- value: toGiB(usedKb * 1024),
88830
- unit: "GiB",
88831
- precision: 1
89275
+ value: bytes(usedKb * 1024),
89276
+ quantity: "information"
88832
89277
  }
88833
89278
  },
88834
89279
  {
@@ -88839,9 +89284,8 @@ function arrayMetricDescriptors(snapshot) {
88839
89284
  name: "Array free",
88840
89285
  role: DeviceRole.NumericSensor,
88841
89286
  numeric: {
88842
- value: toGiB(array.capacityKb.free * 1024),
88843
- unit: "GiB",
88844
- precision: 1
89287
+ value: bytes(array.capacityKb.free * 1024),
89288
+ quantity: "information"
88845
89289
  }
88846
89290
  },
88847
89291
  {
@@ -88853,8 +89297,7 @@ function arrayMetricDescriptors(snapshot) {
88853
89297
  role: DeviceRole.NumericSensor,
88854
89298
  numeric: {
88855
89299
  value: pct(cpu.percent),
88856
- unit: PERCENT,
88857
- precision: 1
89300
+ quantity: "percentage"
88858
89301
  }
88859
89302
  },
88860
89303
  {
@@ -88866,8 +89309,7 @@ function arrayMetricDescriptors(snapshot) {
88866
89309
  role: DeviceRole.NumericSensor,
88867
89310
  numeric: {
88868
89311
  value: pct(array.parityCheck?.progress ?? 0),
88869
- unit: PERCENT,
88870
- precision: 0
89312
+ quantity: "percentage"
88871
89313
  }
88872
89314
  },
88873
89315
  {
@@ -88879,7 +89321,7 @@ function arrayMetricDescriptors(snapshot) {
88879
89321
  role: DeviceRole.NumericSensor,
88880
89322
  numeric: {
88881
89323
  value: Math.max(0, notifications.activeCount),
88882
- precision: 0
89324
+ quantity: "count"
88883
89325
  }
88884
89326
  }
88885
89327
  ];
@@ -88901,8 +89343,7 @@ function arrayMetricDescriptors(snapshot) {
88901
89343
  role: DeviceRole.PowerSensor,
88902
89344
  numeric: {
88903
89345
  value: Math.max(0, cpu.powerW),
88904
- unit: WATT,
88905
- precision: 1
89346
+ quantity: "power"
88906
89347
  }
88907
89348
  });
88908
89349
  return out;
@@ -88986,8 +89427,7 @@ function diskDescriptors(disks) {
88986
89427
  role: DeviceRole.NumericSensor,
88987
89428
  numeric: {
88988
89429
  value: usagePercent,
88989
- unit: PERCENT,
88990
- precision: 1
89430
+ quantity: "percentage"
88991
89431
  }
88992
89432
  });
88993
89433
  out.push({
@@ -89026,8 +89466,7 @@ function dockerDescriptors(containers) {
89026
89466
  role: DeviceRole.NumericSensor,
89027
89467
  numeric: {
89028
89468
  value: pct(container.cpuPercent),
89029
- unit: PERCENT,
89030
- precision: 1
89469
+ quantity: "percentage"
89031
89470
  }
89032
89471
  });
89033
89472
  out.push({
@@ -89039,8 +89478,7 @@ function dockerDescriptors(containers) {
89039
89478
  role: DeviceRole.NumericSensor,
89040
89479
  numeric: {
89041
89480
  value: pct(container.memPercent),
89042
- unit: PERCENT,
89043
- precision: 1
89481
+ quantity: "percentage"
89044
89482
  }
89045
89483
  });
89046
89484
  out.push(buttonDescriptor(`restart_container_${slug}`, `Restart ${container.name}`, {
@@ -89222,10 +89660,9 @@ var UnraidEntityDevice = class extends BaseDevice {
89222
89660
  if (numeric === void 0) return;
89223
89661
  const slice = {
89224
89662
  value: numeric.value,
89225
- lastFetchedAt: now,
89226
- ...numeric.unit !== void 0 ? { unit: numeric.unit } : {},
89227
- ...numeric.precision !== void 0 ? { precision: numeric.precision } : {}
89663
+ lastFetchedAt: now
89228
89664
  };
89665
+ if (this.sourceInfo.quantity !== numeric.quantity) this.updateSourceInfo({ quantity: numeric.quantity });
89229
89666
  this.runtimeState.setCapState(numericSensorCapability.name, slice);
89230
89667
  return;
89231
89668
  }