@camstack/addon-provider-unifi 0.2.146 → 0.2.148

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. */
@@ -58885,6 +59259,7 @@ var doorbellCustomize = {
58885
59259
  applyAuthority: { kind: "composition-claim" },
58886
59260
  eventName: "onPressed",
58887
59261
  edges: ["rising"],
59262
+ defaultEdge: "rising",
58888
59263
  timestampPath: "lastPressedAt",
58889
59264
  counterPath: "pressCountSinceStart",
58890
59265
  highWater: "composer-advance",
@@ -58903,7 +59278,7 @@ var DoorbellStatusSchema = object({
58903
59278
  /** Ms epoch of the last press. null = never observed since this provider started. */
58904
59279
  lastPressedAt: number().nullable(),
58905
59280
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58906
- pressCountSinceStart: number()
59281
+ pressCountSinceStart: number().meta(readingMeta("count"))
58907
59282
  });
58908
59283
  var DoorbellPressEventSchema = object({
58909
59284
  deviceId: number(),
@@ -58968,17 +59343,7 @@ onPressed: {
58968
59343
  history: "none",
58969
59344
  customize: doorbellCustomize
58970
59345
  };
58971
- /**
58972
- * Enum-state sensor — a string value picked from a finite option set.
58973
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58974
- * states, weather conditions, contact-source channels, mode strings).
58975
- * The option set is stable across observations; it is persisted in the
58976
- * device config blob (via the control slice at adoption) and is NOT
58977
- * stored in `sourceInfo`.
58978
- */
58979
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58980
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58981
- var EnumSensorDateTimeFormatSchema = _enum([
59346
+ _enum([
58982
59347
  "date",
58983
59348
  "time",
58984
59349
  "datetime"
@@ -58986,12 +59351,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58986
59351
  var EnumSensorStatusSchema = object({
58987
59352
  value: string(),
58988
59353
  /**
58989
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58990
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58991
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58992
- * 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.
58993
59357
  */
58994
- format: EnumSensorDateTimeFormatSchema.optional(),
59358
+ timestamp: boolean().optional(),
58995
59359
  /** Ms epoch when the slice was last updated. */
58996
59360
  lastFetchedAt: number()
58997
59361
  });
@@ -59018,11 +59382,11 @@ var enumSensorCapability = {
59018
59382
  * History (D850): the value as an `enum` series (an appliance's program or
59019
59383
  * operation state, an HVAC action). The option set is per device, so the
59020
59384
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59021
- * `format` carries a timestamp, not a state, and is not recorded.
59385
+ * `timestamp` carries a clock, not a state, and is not recorded.
59022
59386
  */
59023
59387
  history: { fields: { value: {
59024
59388
  kind: "enum",
59025
- skipWhenSet: "format"
59389
+ skipWhenSet: "timestamp"
59026
59390
  } } },
59027
59391
  /** Clock fields: written, but excluded from the compare that decides
59028
59392
  * whether persisting is worth a SQLite commit. */
@@ -59042,13 +59406,13 @@ var EventFireSchema = object({
59042
59406
  deviceId: number(),
59043
59407
  eventType: string(),
59044
59408
  data: record(string(), unknown()).nullable(),
59045
- timestamp: number(),
59046
- seq: number()
59409
+ timestamp: number().meta(readingMeta("none")),
59410
+ seq: number().meta(readingMeta("count"))
59047
59411
  });
59048
59412
  var EventEmitterStatusSchema = object({
59049
59413
  eventTypes: array(string()),
59050
59414
  lastEvent: EventFireSchema.nullable(),
59051
- eventCountSinceStart: number()
59415
+ eventCountSinceStart: number().meta(readingMeta("count"))
59052
59416
  });
59053
59417
  var eventEmitterCapability = {
59054
59418
  name: "event-emitter",
@@ -59461,10 +59825,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59461
59825
  var FanControlStatusSchema = object({
59462
59826
  /** Active speed as 0..100 inclusive. Null when the device has no
59463
59827
  * speed surface (single-speed fan). */
59464
- percentage: number().min(0).max(100).nullable(),
59828
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59465
59829
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59466
59830
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59467
- percentageStep: number().positive().optional(),
59831
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59468
59832
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59469
59833
  * — empty string when no preset is active or supported. */
59470
59834
  preset: string(),
@@ -59585,7 +59949,7 @@ var FeatureProbeStatusSchema = object({
59585
59949
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59586
59950
  model: string().nullable(),
59587
59951
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59588
- channelCount: number().nullable(),
59952
+ channelCount: number().nullable().meta(readingMeta("count")),
59589
59953
  /**
59590
59954
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59591
59955
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59708,9 +60072,91 @@ var gasCapability = {
59708
60072
  * whether persisting is worth a SQLite commit. */
59709
60073
  volatileStateFields: ["lastChangedAt"]
59710
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
+ };
59711
60157
  var HeatingValveStatusSchema = object({
59712
60158
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59713
- position: number().min(0).max(100).nullable(),
60159
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59714
60160
  /** Ms epoch when the slice was last updated (push or poll). */
59715
60161
  lastFetchedAt: number()
59716
60162
  });
@@ -59753,9 +60199,9 @@ var HumidifierStatusSchema = object({
59753
60199
  /** Whether the humidifier is currently on. */
59754
60200
  on: boolean(),
59755
60201
  /** Current measured relative humidity (0..100). Null when not reported. */
59756
- currentHumidity: number().min(0).max(100).nullable(),
60202
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59757
60203
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59758
- targetHumidity: number().min(0).max(100).nullable(),
60204
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59759
60205
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59760
60206
  * device has no mode surface. */
59761
60207
  mode: string().nullable(),
@@ -59765,9 +60211,9 @@ var HumidifierStatusSchema = object({
59765
60211
  * `idle` / `off`). Null when the device doesn't report it. */
59766
60212
  action: string().nullable(),
59767
60213
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59768
- minHumidity: number().nullable(),
60214
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59769
60215
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59770
- maxHumidity: number().nullable(),
60216
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59771
60217
  /** Ms epoch when the slice was last updated. */
59772
60218
  lastChangedAt: number()
59773
60219
  });
@@ -59837,22 +60283,13 @@ var humidifierCapability = {
59837
60283
  * `sourceInfo`.
59838
60284
  */
59839
60285
  var HumiditySensorStatusSchema = object({
59840
- /** Current relative humidity, 0..100. */
59841
- percent: number().min(0).max(100).meta({
59842
- 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,
59843
60289
  label: "Humidity"
59844
- }),
60290
+ })),
59845
60291
  /** Ms epoch when the slice was last updated. */
59846
- lastFetchedAt: number(),
59847
- /** Live display unit from the upstream source (e.g. HA
59848
- * `attributes.unit_of_measurement`). The UI prefers this over the
59849
- * role's canonical unit. Absent → fall back to the canonical unit. */
59850
- unit: string().optional(),
59851
- /** Suggested decimal places for numeric display.
59852
- * Populated live from the upstream source when provided (e.g. HA
59853
- * `attributes.suggested_display_precision`). Falls back to
59854
- * auto-formatting when absent. */
59855
- precision: number().int().min(0).max(10).optional()
60292
+ lastFetchedAt: number()
59856
60293
  });
59857
60294
  var humiditySensorCapability = {
59858
60295
  name: "humidity-sensor",
@@ -59883,12 +60320,7 @@ var humiditySensorCapability = {
59883
60320
  } } },
59884
60321
  /** Clock fields: written, but excluded from the compare that decides
59885
60322
  * whether persisting is worth a SQLite commit. */
59886
- volatileStateFields: ["lastFetchedAt"],
59887
- customize: {
59888
- version: 1,
59889
- advanced: "derive",
59890
- targets: []
59891
- }
60323
+ volatileStateFields: ["lastFetchedAt"]
59892
60324
  };
59893
60325
  /**
59894
60326
  * Image display cap. Models a single still image exposed by an integration —
@@ -59909,7 +60341,7 @@ var ImageStatusSchema = object({
59909
60341
  * entity exposes no `entity_picture` (yet). */
59910
60342
  url: string().nullable(),
59911
60343
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59912
- lastUpdated: number().nullable()
60344
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59913
60345
  });
59914
60346
  var imageCapability = {
59915
60347
  name: "image",
@@ -59997,19 +60429,19 @@ var NormalizedRangeSchema = object({
59997
60429
  */
59998
60430
  var ImageSettingsStatusSchema = object({
59999
60431
  /** Normalized 0–100. */
60000
- brightness: number().optional(),
60432
+ brightness: number().optional().meta(readingMeta("none")),
60001
60433
  /** Normalized 0–100. */
60002
- contrast: number().optional(),
60434
+ contrast: number().optional().meta(readingMeta("none")),
60003
60435
  /** Normalized 0–100. */
60004
- saturation: number().optional(),
60436
+ saturation: number().optional().meta(readingMeta("none")),
60005
60437
  /** Normalized 0–100. */
60006
- sharpness: number().optional(),
60438
+ sharpness: number().optional().meta(readingMeta("none")),
60007
60439
  mirror: boolean().optional(),
60008
60440
  flip: boolean().optional(),
60009
60441
  rotate: ImageRotateSchema.optional(),
60010
60442
  whiteBalance: WhiteBalanceModeSchema.optional(),
60011
60443
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60012
- warmth: number().optional(),
60444
+ warmth: number().optional().meta(readingMeta("none")),
60013
60445
  exposureMode: ExposureModeSchema.optional(),
60014
60446
  backlightMode: BacklightModeSchema.optional(),
60015
60447
  lastFetchedAt: number()
@@ -60288,10 +60720,10 @@ var IntercomAbilitySchema = object({
60288
60720
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60289
60721
  codecs: array(string()),
60290
60722
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60291
- sampleRate: number(),
60723
+ sampleRate: number().meta(readingMeta("none")),
60292
60724
  duplex: _enum(["half", "full"]),
60293
60725
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60294
- maxBacklogMs: number()
60726
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60295
60727
  });
60296
60728
  var IntercomStatusSchema = object({
60297
60729
  /** True while a talk session is open. */
@@ -60518,13 +60950,13 @@ var LawnMowerControlStatusSchema = object({
60518
60950
  activity: LawnMowerActivitySchema,
60519
60951
  /** 0..100 battery percentage. Null when the device has no battery
60520
60952
  * reading. */
60521
- batteryLevel: number().min(0).max(100).nullable(),
60953
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60522
60954
  /** 0..100 mowing-completion percentage of the current task, or null when no
60523
60955
  * task is active / progress is unavailable. */
60524
- progressPercent: number().min(0).max(100).nullable(),
60956
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60525
60957
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60526
60958
  * or null when unknown. */
60527
- currentCode: number().nullable(),
60959
+ currentCode: number().nullable().meta(readingMeta("none")),
60528
60960
  /** Human label for {@link currentCode}, or null when undecodable. */
60529
60961
  currentCodeLabel: string().nullable(),
60530
60962
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61057,7 +61489,7 @@ var MediaPlayerStatusSchema = object({
61057
61489
  state: MediaPlayerStateSchema,
61058
61490
  /** Volume as 0..100 inclusive. Null when the device has no volume
61059
61491
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61060
- volumeLevel: number().min(0).max(100).nullable(),
61492
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61061
61493
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61062
61494
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61063
61495
  isMuted: boolean().nullable(),
@@ -61070,10 +61502,10 @@ var MediaPlayerStatusSchema = object({
61070
61502
  currentMedia: MediaInfoSchema.nullable(),
61071
61503
  /** Current playback position in ms. Null when not seekable or
61072
61504
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61073
- positionMs: number().int().nonnegative().nullable(),
61505
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61074
61506
  /** Total duration of the current media in ms. Null when unknown
61075
61507
  * (live stream) or nothing is playing. */
61076
- durationMs: number().int().nonnegative().nullable(),
61508
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61077
61509
  /** Shuffle toggle. Null when no shuffle surface.
61078
61510
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61079
61511
  shuffle: boolean().nullable(),
@@ -61382,7 +61814,7 @@ var motionCustomize = {
61382
61814
  eventName: "onMotionChanged"
61383
61815
  },
61384
61816
  deviceTypes: [DeviceType.Camera],
61385
- apply: "replace",
61817
+ apply: "add",
61386
61818
  applyAuthority: {
61387
61819
  kind: "camera-sensors",
61388
61820
  timeoutField: "motionCooldownMs"
@@ -61454,7 +61886,7 @@ var MotionStatusSchema = object({
61454
61886
  * it, every provider seeded it at boot, and every session left it
61455
61887
  * `detected: true` after its detach.
61456
61888
  */
61457
- autoClearAfterMs: number().nullable()
61889
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61458
61890
  });
61459
61891
  /**
61460
61892
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61491,12 +61923,18 @@ var motionCapability = {
61491
61923
  methods: {
61492
61924
  /**
61493
61925
  * Pull the current motion state synchronously. Convenience shortcut
61494
- * for consumers that don't need the full status object; equivalent
61495
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61496
- * `getStatus` once the auto-injected status surface lands in every
61497
- * 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.
61498
61928
  */
61499
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
+ } },
61500
61938
  events: {
61501
61939
  /**
61502
61940
  * Fires every time the runner transitions a camera between
@@ -61636,7 +62074,7 @@ var MotionZoneRegionSchema = object({
61636
62074
  * the grid region(s). */
61637
62075
  var MotionZoneStatusSchema = object({
61638
62076
  enabled: boolean(),
61639
- sensitivity: number(),
62077
+ sensitivity: number().meta(readingMeta("none")),
61640
62078
  /** Grid region(s). Today exactly one `grid` shape. */
61641
62079
  regions: array(MotionZoneRegionSchema),
61642
62080
  lastFetchedAt: number()
@@ -62006,7 +62444,7 @@ var NavigationStatusSchema = object({
62006
62444
  following: boolean(),
62007
62445
  flash: boolean(),
62008
62446
  lightMode: NavigationLightModeSchema,
62009
- lightLevel: number().min(40).max(100),
62447
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62010
62448
  /** Ms epoch when the slice was last updated. */
62011
62449
  lastChangedAt: number()
62012
62450
  });
@@ -62140,13 +62578,13 @@ var NetworkLinkStatusSchema = object({
62140
62578
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62141
62579
  * SKIP a null rather than coerce it.
62142
62580
  */
62143
- signalPercent: number().min(0).max(100).nullable(),
62581
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62144
62582
  /** Raw received signal strength in dBm, when the firmware reports one. */
62145
- rssiDbm: number().optional(),
62583
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62146
62584
  /** Network name of a wireless link, when the firmware reports it. */
62147
62585
  ssid: string().optional(),
62148
62586
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62149
- lastUpdated: number()
62587
+ lastUpdated: number().meta(readingMeta("none"))
62150
62588
  });
62151
62589
  var networkLinkCapability = {
62152
62590
  name: "network-link",
@@ -62531,7 +62969,7 @@ var NotifierStatusSchema = object({
62531
62969
  lastError: string().nullable(),
62532
62970
  /** Number of deliveries currently buffered server-side (rate-limited
62533
62971
  * notifiers may queue). 0 when the provider sends synchronously. */
62534
- queueDepth: number().int().nonnegative(),
62972
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62535
62973
  /** Per-feature capability matrix — authoritative source for the
62536
62974
  * compose-form field gating. */
62537
62975
  supports: NotifierSupportsSchema
@@ -62623,11 +63061,9 @@ onSent: { data: object({
62623
63061
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62624
63062
  * for: water flow, distance, weight, frequency, signal strength, …).
62625
63063
  *
62626
- * The `unit` and `precision` fields carry the live display metadata that
62627
- * the upstream source provides with each state push (HA
62628
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62629
- * The UI reads them directly from the slice — they are the single source
62630
- * 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).
62631
63067
  *
62632
63068
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62633
63069
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62636,15 +63072,8 @@ onSent: { data: object({
62636
63072
  * numeric values land here.
62637
63073
  */
62638
63074
  var NumericSensorStatusSchema = object({
62639
- value: number(),
62640
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62641
- * Populated live from the upstream source on each state push.
62642
- * Absent for unitless sensors. */
62643
- unit: string().optional(),
62644
- /** Suggested decimal places for numeric display.
62645
- * Populated live from the upstream source when provided.
62646
- * Falls back to auto-formatting when absent. */
62647
- 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")),
62648
63077
  /** Ms epoch when the slice was last updated. */
62649
63078
  lastFetchedAt: number()
62650
63079
  });
@@ -62668,11 +63097,11 @@ var numericSensorCapability = {
62668
63097
  */
62669
63098
  durability: "restored",
62670
63099
  /**
62671
- * 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.
62672
63102
  */
62673
63103
  history: { fields: { value: {
62674
63104
  kind: "gauge",
62675
- unitFrom: "unit",
62676
63105
  minIntervalMs: 3e4
62677
63106
  } } },
62678
63107
  /** Clock fields: written, but excluded from the compare that decides
@@ -62964,23 +63393,26 @@ var PetFeederStatusSchema = object({
62964
63393
  /** Food currently in the bowl (grams). Null when the device has not
62965
63394
  * reported a reading yet. On dual-hopper models this is the combined
62966
63395
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62967
- foodLevel: number().nullable(),
63396
+ foodLevel: number().nullable().meta(readingMeta("none")),
62968
63397
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62969
63398
  * single-hopper models. */
62970
- food1: number().nullable(),
63399
+ food1: number().nullable().meta(readingMeta("none")),
62971
63400
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62972
63401
  * single-hopper models. */
62973
- food2: number().nullable(),
63402
+ food2: number().nullable().meta(readingMeta("none")),
62974
63403
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62975
63404
  * (`device_class: problem`, on = low). True when the bowl is empty /
62976
63405
  * below the feeder's low threshold. */
62977
63406
  lowFood: boolean(),
62978
63407
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62979
63408
  * device has no battery reading. */
62980
- batteryPower: number().min(0).max(100).nullable(),
62981
- /** Days of desiccant life remaining. Null when the model has no
62982
- * desiccant sensor. */
62983
- 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")),
62984
63416
  /** True while a feed is in progress. */
62985
63417
  feeding: boolean(),
62986
63418
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62991,7 +63423,7 @@ var PetFeederStatusSchema = object({
62991
63423
  * with `errorCode` for consumers that want the raw integer. */
62992
63424
  error: string().nullable(),
62993
63425
  /** Raw device error code (0 / null = no error). */
62994
- errorCode: number().nullable(),
63426
+ errorCode: number().nullable().meta(readingMeta("none")),
62995
63427
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62996
63428
  isDualHopper: boolean(),
62997
63429
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63001,7 +63433,7 @@ var PetFeederStatusSchema = object({
63001
63433
  /** Play a chime when dispensing. */
63002
63434
  feedSound: boolean(),
63003
63435
  /** Speaker / prompt volume level (device-scaled integer). */
63004
- volume: number(),
63436
+ volume: number().meta(readingMeta("none")),
63005
63437
  /** Ms epoch when the slice was last refreshed from the cloud. */
63006
63438
  lastFetchedAt: number()
63007
63439
  });
@@ -63120,7 +63552,7 @@ var petFeederCapability = {
63120
63552
  },
63121
63553
  desiccantLeftDays: {
63122
63554
  kind: "gauge",
63123
- unit: "days"
63555
+ unit: "s"
63124
63556
  }
63125
63557
  } }
63126
63558
  };
@@ -63521,36 +63953,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63521
63953
  */
63522
63954
  var PowerMeterStatusSchema = object({
63523
63955
  /** Instantaneous power draw in watts. */
63524
- watts: number().optional().meta({
63525
- unit: "W",
63526
- label: "Power"
63527
- }),
63956
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63528
63957
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63529
- kwhTotal: number().optional().meta({
63530
- unit: "kWh",
63531
- label: "Energy"
63532
- }),
63958
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63533
63959
  /** Voltage in volts. */
63534
- volts: number().optional(),
63960
+ volts: number().optional().meta(readingMeta("voltage")),
63535
63961
  /** Current in amperes. */
63536
- amps: number().optional(),
63537
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63538
- va: number().optional(),
63962
+ amps: number().optional().meta(readingMeta("current")),
63963
+ /** Apparent power in volt-amperes. */
63964
+ va: number().optional().meta(readingMeta("apparent-power")),
63539
63965
  /** Power factor (0..1) if reported. */
63540
- powerFactor: number().min(0).max(1).optional(),
63966
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63541
63967
  /** Ms epoch when the slice was last updated. */
63542
- lastFetchedAt: number(),
63543
- /** Live display unit of the single metric this slice carries (e.g. HA
63544
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63545
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63546
- * unit per slice is unambiguous. The UI prefers this over the role's
63547
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63548
- unit: string().optional(),
63549
- /** Suggested decimal places for numeric display.
63550
- * Populated live from the upstream source when provided (e.g. HA
63551
- * `attributes.suggested_display_precision`). Falls back to
63552
- * auto-formatting when absent. */
63553
- precision: number().int().min(0).max(10).optional()
63968
+ lastFetchedAt: number()
63554
63969
  });
63555
63970
  var powerMeterCapability = {
63556
63971
  name: "power-meter",
@@ -63614,12 +64029,7 @@ var powerMeterCapability = {
63614
64029
  } },
63615
64030
  /** Clock fields: written, but excluded from the compare that decides
63616
64031
  * whether persisting is worth a SQLite commit. */
63617
- volatileStateFields: ["lastFetchedAt"],
63618
- customize: {
63619
- version: 1,
63620
- advanced: "derive",
63621
- targets: []
63622
- }
64032
+ volatileStateFields: ["lastFetchedAt"]
63623
64033
  };
63624
64034
  /**
63625
64035
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63639,11 +64049,11 @@ var powerMeterCapability = {
63639
64049
  */
63640
64050
  var GpsLocationSchema = object({
63641
64051
  /** Latitude in decimal degrees, -90..90. */
63642
- latitude: number().min(-90).max(90),
64052
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63643
64053
  /** Longitude in decimal degrees, -180..180. */
63644
- longitude: number().min(-180).max(180),
64054
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63645
64055
  /** Reported accuracy in meters (lower = better). */
63646
- accuracyMeters: number().nonnegative()
64056
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63647
64057
  });
63648
64058
  var PresenceStatusSchema = object({
63649
64059
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63655,7 +64065,7 @@ var PresenceStatusSchema = object({
63655
64065
  gps: GpsLocationSchema.nullable(),
63656
64066
  /** Optional battery level of the tracking device (0..100). Null
63657
64067
  * when not reported. */
63658
- batteryPercent: number().min(0).max(100).nullable(),
64068
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63659
64069
  /** Ms epoch when the slice was last updated. */
63660
64070
  lastChangedAt: number()
63661
64071
  });
@@ -63705,19 +64115,10 @@ var presenceCapability = {
63705
64115
  * `sourceInfo`.
63706
64116
  */
63707
64117
  var PressureSensorStatusSchema = object({
63708
- /** Current pressure in hPa. */
63709
- hpa: number(),
64118
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64119
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63710
64120
  /** Ms epoch when the slice was last updated. */
63711
- lastFetchedAt: number(),
63712
- /** Live display unit from the upstream source (e.g. HA
63713
- * `attributes.unit_of_measurement`). The UI prefers this over the
63714
- * role's canonical unit. Absent → fall back to the canonical unit. */
63715
- unit: string().optional(),
63716
- /** Suggested decimal places for numeric display.
63717
- * Populated live from the upstream source when provided (e.g. HA
63718
- * `attributes.suggested_display_precision`). Falls back to
63719
- * auto-formatting when absent. */
63720
- precision: number().int().min(0).max(10).optional()
64121
+ lastFetchedAt: number()
63721
64122
  });
63722
64123
  var pressureSensorCapability = {
63723
64124
  name: "pressure-sensor",
@@ -64131,8 +64532,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64131
64532
  }), _void(), { kind: "mutation" });
64132
64533
  var PtzAutotrackSettingsSchema = object({
64133
64534
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64134
- stopDelaySeconds: number().int().min(0).max(300),
64135
- 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"))
64136
64537
  });
64137
64538
  /**
64138
64539
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64286,7 +64687,7 @@ var RunningStateStatusSchema = object({
64286
64687
  /** null = unknown — never coerced to false (D393). */
64287
64688
  running: boolean().nullable(),
64288
64689
  /** Epoch ms the current boolean began; null while `running` is null. */
64289
- since: number().nullable(),
64690
+ since: number().nullable().meta(readingMeta("none")),
64290
64691
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64291
64692
  phase: string().nullable(),
64292
64693
  /** null until a reading has been published (D393). */
@@ -64301,7 +64702,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64301
64702
  /** Generic Customize target for the running boolean (D936). */
64302
64703
  var runningCustomize = {
64303
64704
  version: 1,
64304
- advanced: "derive",
64305
64705
  targets: [{
64306
64706
  id: "running",
64307
64707
  cardinality: "scalar",
@@ -64321,7 +64721,7 @@ var runningCustomize = {
64321
64721
  DeviceType.Thermostat,
64322
64722
  DeviceType.PetFeeder
64323
64723
  ],
64324
- apply: "add-or-replace",
64724
+ apply: "add",
64325
64725
  path: "running",
64326
64726
  sources: [
64327
64727
  {
@@ -64337,6 +64737,7 @@ var runningCustomize = {
64337
64737
  requiredUnit: "W",
64338
64738
  transforms: [{
64339
64739
  kind: "threshold",
64740
+ comparisons: ["gte", "lte"],
64340
64741
  comparison: "gte",
64341
64742
  defaultThreshold: 5,
64342
64743
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -65584,9 +65985,9 @@ var RecordTriggerSchema = _enum([
65584
65985
  */
65585
65986
  var RecordWindowSchema = object({
65586
65987
  trigger: RecordTriggerSchema,
65587
- day: number().int().min(0).max(6),
65588
- startMinute: number().int().min(0).max(1439),
65589
- 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"))
65590
65991
  });
65591
65992
  /** Status of one physical volume, as the camera itself describes it. */
65592
65993
  var OnboardStorageVolumeSchema = object({
@@ -65662,10 +66063,10 @@ var RecordingOnboardStatusSchema = object({
65662
66063
  /** Master "record to the card at all" switch. */
65663
66064
  enabled: boolean().nullable(),
65664
66065
  overwriteWhenFull: boolean().nullable(),
65665
- preRecordSec: number().nullable(),
65666
- postRecordSec: number().nullable(),
66066
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66067
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65667
66068
  /** Length of one recorded file, in minutes. */
65668
- segmentMinutes: number().nullable(),
66069
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65669
66070
  /** The primary track's weekly windows, flattened. */
65670
66071
  windows: array(RecordWindowSchema),
65671
66072
  /**
@@ -65681,7 +66082,7 @@ var RecordingOnboardStatusSchema = object({
65681
66082
  * provider reporting a non-zero count also reports the schedule as not
65682
66083
  * writable.
65683
66084
  */
65684
- unreadableWindows: number(),
66085
+ unreadableWindows: number().meta(readingMeta("count")),
65685
66086
  /**
65686
66087
  * The camera is scheduled to record and has NO usable storage.
65687
66088
  *
@@ -66526,7 +66927,7 @@ var CamStreamDescriptorSchema = object({
66526
66927
  url: string().optional(),
66527
66928
  codec: string().optional(),
66528
66929
  resolution: CamStreamResolutionSchema.optional(),
66529
- fps: number().positive().optional(),
66930
+ fps: number().positive().optional().meta(readingMeta("rate")),
66530
66931
  label: string().optional(),
66531
66932
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66532
66933
  deviceFeatures: array(string()).optional(),
@@ -66613,18 +67014,18 @@ var StreamProfileSchema = _enum([
66613
67014
  ]);
66614
67015
  /** The current encoding config of one stream profile. */
66615
67016
  var StreamProfileConfigSchema = object({
66616
- width: number(),
66617
- height: number(),
67017
+ width: number().meta(readingMeta("none")),
67018
+ height: number().meta(readingMeta("none")),
66618
67019
  codec: _enum(["h264", "h265"]),
66619
- framerate: number(),
66620
- bitrate: number(),
67020
+ framerate: number().meta(readingMeta("rate")),
67021
+ bitrate: number().meta(readingMeta("bitrate")),
66621
67022
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66622
67023
  encoderProfile: _enum([
66623
67024
  "high",
66624
67025
  "main",
66625
67026
  "baseline"
66626
67027
  ]).optional(),
66627
- gop: number().optional(),
67028
+ gop: number().optional().meta(readingMeta("none")),
66628
67029
  /**
66629
67030
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66630
67031
  *
@@ -66845,12 +67246,7 @@ var switchCapability = {
66845
67246
  label: "On",
66846
67247
  kind: "boolean"
66847
67248
  }
66848
- }] },
66849
- customize: {
66850
- version: 1,
66851
- advanced: "derive",
66852
- targets: []
66853
- }
67249
+ }] }
66854
67250
  };
66855
67251
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66856
67252
  function hasControlCharacter(value) {
@@ -67433,35 +67829,21 @@ var tamperCapability = {
67433
67829
  * entries with `device_class: temperature` and any future native
67434
67830
  * thermometer.
67435
67831
  *
67436
- * Unit normalisation: providers convert to Celsius before storing.
67437
- * The slice value is always Celsius so cross-cap aggregators
67438
- * (climate-control's `currentTemp`, energy analytics) can compose
67439
- * without per-source unit fixups. The canonical display unit (`°C`) is
67440
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67441
- * `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.
67442
67834
  *
67443
67835
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67444
67836
  * frozen feed (provider hung) distinct from a "temperature hasn't
67445
67837
  * changed" steady state.
67446
67838
  */
67447
67839
  var TemperatureSensorStatusSchema = object({
67448
- /** Current temperature in Celsius. */
67449
- celsius: number().meta({
67450
- unit: "°C",
67840
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67841
+ celsius: number().nullable().meta(readingMeta("temperature", {
67842
+ precision: 1,
67451
67843
  label: "Temperature"
67452
- }),
67844
+ })),
67453
67845
  /** Ms epoch when the slice was last updated (push or poll). */
67454
- lastFetchedAt: number(),
67455
- /** Live display unit from the upstream source (e.g. HA
67456
- * `attributes.unit_of_measurement`). The UI prefers this over the
67457
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67458
- * Absent → the UI falls back to the role's canonical unit. */
67459
- unit: string().optional(),
67460
- /** Suggested decimal places for numeric display.
67461
- * Populated live from the upstream source when provided (e.g. HA
67462
- * `attributes.suggested_display_precision`). Falls back to
67463
- * auto-formatting when absent. */
67464
- precision: number().int().min(0).max(10).optional()
67846
+ lastFetchedAt: number()
67465
67847
  });
67466
67848
  var temperatureSensorCapability = {
67467
67849
  name: "temperature-sensor",
@@ -67492,12 +67874,7 @@ var temperatureSensorCapability = {
67492
67874
  } } },
67493
67875
  /** Clock fields: written, but excluded from the compare that decides
67494
67876
  * whether persisting is worth a SQLite commit. */
67495
- volatileStateFields: ["lastFetchedAt"],
67496
- customize: {
67497
- version: 1,
67498
- advanced: "derive",
67499
- targets: []
67500
- }
67877
+ volatileStateFields: ["lastFetchedAt"]
67501
67878
  };
67502
67879
  /**
67503
67880
  * toast — system-scoped singleton capability that streams toast
@@ -67780,16 +68157,16 @@ var VacuumControlStatusSchema = object({
67780
68157
  ]),
67781
68158
  /** 0..100 battery percentage. Null when the device has no battery
67782
68159
  * reading. */
67783
- batteryLevel: number().min(0).max(100).nullable(),
68160
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67784
68161
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67785
68162
  * the vacuum has no speed control. */
67786
68163
  fanSpeed: string().nullable(),
67787
68164
  /** Speed tokens the hardware accepts — drives the UI selector. */
67788
68165
  availableFanSpeeds: array(string()),
67789
68166
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67790
- progressPercent: number().min(0).max(100).nullable(),
68167
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67791
68168
  /** Current error code (0 / null = no error). */
67792
- errorCode: number().nullable(),
68169
+ errorCode: number().nullable().meta(readingMeta("none")),
67793
68170
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67794
68171
  errorLabel: string().nullable(),
67795
68172
  /** Ms epoch when the slice was last updated. */
@@ -67867,7 +68244,7 @@ var ValveStatusSchema = object({
67867
68244
  ]),
67868
68245
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67869
68246
  * intermediate position surface. */
67870
- position: number().min(0).max(100).nullable(),
68247
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67871
68248
  /** Ms epoch when the slice was last updated. */
67872
68249
  lastChangedAt: number()
67873
68250
  });
@@ -67975,9 +68352,9 @@ var vibrationCapability = {
67975
68352
  */
67976
68353
  var WaterHeaterStatusSchema = object({
67977
68354
  /** Current measured temperature. Null when not reported. */
67978
- currentTemp: number().nullable(),
68355
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67979
68356
  /** Target temperature setpoint. Null when no setpoint surface. */
67980
- targetTemp: number().nullable(),
68357
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67981
68358
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67982
68359
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67983
68360
  * device reports an unknown state. */
@@ -67988,9 +68365,9 @@ var WaterHeaterStatusSchema = object({
67988
68365
  * has no away surface. */
67989
68366
  away: boolean().nullable(),
67990
68367
  /** HA `min_temp` attribute. Null when not reported. */
67991
- minTemp: number().nullable(),
68368
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67992
68369
  /** HA `max_temp` attribute. Null when not reported. */
67993
- maxTemp: number().nullable(),
68370
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67994
68371
  /** Ms epoch when the slice was last updated. */
67995
68372
  lastChangedAt: number()
67996
68373
  });
@@ -68075,25 +68452,18 @@ var waterHeaterCapability = {
68075
68452
  * Forecast deferred — current conditions only for v1.
68076
68453
  */
68077
68454
  var WeatherStatusSchema = object({
68078
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68079
- * when no condition has been reported yet. */
68455
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68080
68456
  condition: string().nullable(),
68081
- /** Current temperature in the reported unit. Null when not provided. */
68082
- temperature: number().nullable(),
68083
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68084
- temperatureUnit: string().nullable(),
68457
+ /** Current temperature in Celsius. Null when not provided or refused. */
68458
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68085
68459
  /** Relative humidity (0..100). Null when not provided. */
68086
- humidity: number().min(0).max(100).nullable(),
68087
- /** Barometric pressure in the reported unit. Null when not provided. */
68088
- pressure: number().nullable(),
68089
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68090
- pressureUnit: string().nullable(),
68091
- /** Wind speed in the reported unit. Null when not provided. */
68092
- windSpeed: number().nullable(),
68093
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68094
- 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")),
68095
68465
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68096
- windBearing: number().nullable(),
68466
+ windBearing: number().nullable().meta(readingMeta("angle")),
68097
68467
  /** Ms epoch when the slice was last updated. */
68098
68468
  lastFetchedAt: number()
68099
68469
  });
@@ -68121,12 +68491,12 @@ var weatherCapability = {
68121
68491
  */
68122
68492
  durability: "session",
68123
68493
  /**
68124
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68494
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68125
68495
  */
68126
68496
  history: { fields: {
68127
68497
  temperature: {
68128
68498
  kind: "gauge",
68129
- unitFrom: "temperatureUnit",
68499
+ unit: "°C",
68130
68500
  deadband: .2
68131
68501
  },
68132
68502
  humidity: {
@@ -68136,12 +68506,12 @@ var weatherCapability = {
68136
68506
  },
68137
68507
  pressure: {
68138
68508
  kind: "gauge",
68139
- unitFrom: "pressureUnit",
68509
+ unit: "hPa",
68140
68510
  deadband: .5
68141
68511
  },
68142
68512
  windSpeed: {
68143
68513
  kind: "gauge",
68144
- unitFrom: "windSpeedUnit",
68514
+ unit: "m/s",
68145
68515
  deadband: .5
68146
68516
  }
68147
68517
  } }
@@ -69372,6 +69742,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69372
69742
  featureProbe: featureProbeCapability,
69373
69743
  flood: floodCapability,
69374
69744
  gas: gasCapability,
69745
+ groupCommands: groupCommandsCapability,
69746
+ groupStatus: groupStatusCapability,
69375
69747
  heatingValve: heatingValveCapability,
69376
69748
  humidifier: humidifierCapability,
69377
69749
  humiditySensor: humiditySensorCapability,
@@ -70748,6 +71120,13 @@ Object.freeze({
70748
71120
  access: "view",
70749
71121
  role: "admin"
70750
71122
  },
71123
+ "addons.getPackageDocs": {
71124
+ capName: "addons",
71125
+ capScope: "system",
71126
+ addonId: null,
71127
+ access: "view",
71128
+ role: "admin"
71129
+ },
70751
71130
  "addons.getVersions": {
70752
71131
  capName: "addons",
70753
71132
  capScope: "system",
@@ -73856,6 +74235,20 @@ Object.freeze({
73856
74235
  access: "view",
73857
74236
  role: "viewer"
73858
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
+ },
73859
74252
  "heatingValve.getStatus": {
73860
74253
  capName: "heating-valve",
73861
74254
  capScope: "device",
@@ -83678,6 +84071,37 @@ var SENSOR_CAP_VOCABULARY = {
83678
84071
  icon: "flame"
83679
84072
  }]
83680
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
+ },
83681
84105
  "humidity-sensor": {
83682
84106
  scope: "device",
83683
84107
  noun: t("Humidity", "Umidità"),
@@ -83711,7 +84135,6 @@ var SENSOR_CAP_VOCABULARY = {
83711
84135
  field: "value",
83712
84136
  quantity: t("the reading", "il valore"),
83713
84137
  label: t("Reading", "Valore"),
83714
- unitField: "unit",
83715
84138
  icon: "gauge"
83716
84139
  }]
83717
84140
  },
@@ -83847,7 +84270,7 @@ var SENSOR_CAP_VOCABULARY = {
83847
84270
  field: "temperature",
83848
84271
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83849
84272
  label: t("Temperature", "Temperatura"),
83850
- unitField: "temperatureUnit",
84273
+ unit: "°C",
83851
84274
  icon: "thermometer"
83852
84275
  },
83853
84276
  {
@@ -83863,7 +84286,7 @@ var SENSOR_CAP_VOCABULARY = {
83863
84286
  field: "windSpeed",
83864
84287
  quantity: t("the wind speed", "la velocità del vento"),
83865
84288
  label: t("Wind", "Vento"),
83866
- unitField: "windSpeedUnit",
84289
+ unit: "m/s",
83867
84290
  icon: "wind"
83868
84291
  },
83869
84292
  {
@@ -83871,7 +84294,7 @@ var SENSOR_CAP_VOCABULARY = {
83871
84294
  field: "pressure",
83872
84295
  quantity: t("the air pressure", "la pressione atmosferica"),
83873
84296
  label: t("Pressure", "Pressione"),
83874
- unitField: "pressureUnit",
84297
+ unit: "hPa",
83875
84298
  icon: "gauge"
83876
84299
  }
83877
84300
  ]
@@ -85144,6 +85567,16 @@ Object.freeze({
85144
85567
  form: "single",
85145
85568
  optional: false
85146
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
+ }],
85147
85580
  "heatingValve.getStatus": [{
85148
85581
  name: "deviceId",
85149
85582
  form: "single",
@@ -87008,6 +87441,11 @@ var PulseOverrideDraftSchema = object({
87008
87441
  targetId: string().min(1),
87009
87442
  enabled: boolean(),
87010
87443
  debounceMs: number().nonnegative(),
87444
+ edges: array(_enum([
87445
+ "rising",
87446
+ "button",
87447
+ "native-event"
87448
+ ])).min(1).optional(),
87011
87449
  sources: array(OverrideSourceSchema).min(1)
87012
87450
  });
87013
87451
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -87123,6 +87561,8 @@ new Map(Object.entries({
87123
87561
  "detection-blind": "device-events",
87124
87562
  "motion-noisy": "device-events",
87125
87563
  "motion-calm": "device-events",
87564
+ "package-llm-silent": "device-events",
87565
+ "package-llm-answered": "device-events",
87126
87566
  "recording-stalled": "device-events",
87127
87567
  "recording-healthy": "device-events",
87128
87568
  "recording-degraded": "device-events",
@@ -87206,6 +87646,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87206
87646
  label: "Motion without a subject",
87207
87647
  kinds: ["motion-noisy", "motion-calm"]
87208
87648
  },
87649
+ {
87650
+ id: "package-llm",
87651
+ family: "device-events",
87652
+ label: "Package confirm",
87653
+ kinds: ["package-llm-silent", "package-llm-answered"]
87654
+ },
87209
87655
  {
87210
87656
  id: "recording",
87211
87657
  family: "device-events",
@@ -87387,6 +87833,8 @@ var DEVICE_EVENTS = [
87387
87833
  "detection-blind",
87388
87834
  "motion-noisy",
87389
87835
  "motion-calm",
87836
+ "package-llm-silent",
87837
+ "package-llm-answered",
87390
87838
  "recording-stalled",
87391
87839
  "recording-healthy",
87392
87840
  "recording-degraded",
@@ -87655,6 +88103,15 @@ new Map([
87655
88103
  snapshotLabel: "Cameras with constant motion now",
87656
88104
  snapshotSection: null
87657
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
+ },
87658
88115
  {
87659
88116
  id: "recording-health",
87660
88117
  family: "device-events",