@camstack/addon-matter-broker 0.2.147 → 0.2.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -13,7 +13,7 @@ let node_stream_promises = require("node:stream/promises");
13
13
  let node_net = require("node:net");
14
14
  let node_dgram = require("node:dgram");
15
15
  node_dgram = require_esm.__toESM(node_dgram, 1);
16
- //#region ../types/dist/event-category-DHXxB8t9.mjs
16
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
17
17
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
18
18
  EventCategory["SystemBoot"] = "system.boot";
19
19
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -339,6 +339,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
339
339
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
340
340
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
341
341
  /**
342
+ * A camera's package LLM confirm has failed closed three times in a row
343
+ * (D931, 2026-10-26). One edge per camera until
344
+ * {@link PipelinePackageLlmAnswered}.
345
+ */
346
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
347
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
348
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
349
+ /**
342
350
  * Per-camera pipeline config was mutated by the orchestrator
343
351
  * (a settings change via `setAgentInferenceDevices` /
344
352
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5581,7 +5589,7 @@ var ZodIssueCode = {
5581
5589
  var ZodFirstPartyTypeKind;
5582
5590
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5583
5591
  //#endregion
5584
- //#region ../types/dist/sleep-BErAxhul.mjs
5592
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5585
5593
  /**
5586
5594
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5587
5595
  * window to float samples (D455).
@@ -8323,6 +8331,99 @@ function defineEntryKind(name, spec) {
8323
8331
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8324
8332
  return defined;
8325
8333
  }
8334
+ var READING_QUANTITIES = [
8335
+ "temperature",
8336
+ "pressure",
8337
+ "speed",
8338
+ "precipitation",
8339
+ "precipitation-rate",
8340
+ "length",
8341
+ "area",
8342
+ "illuminance",
8343
+ "irradiance",
8344
+ "power",
8345
+ "energy",
8346
+ "voltage",
8347
+ "current",
8348
+ "apparent-power",
8349
+ "humidity",
8350
+ "percentage",
8351
+ "concentration-volume",
8352
+ "concentration-mass",
8353
+ "concentration-ppb",
8354
+ "volume",
8355
+ "duration",
8356
+ "duration-ms",
8357
+ "information",
8358
+ "sound-level",
8359
+ "signal-level",
8360
+ "colour-temperature",
8361
+ "angle",
8362
+ "bitrate",
8363
+ "rate",
8364
+ "mass",
8365
+ "index",
8366
+ "ratio",
8367
+ "count",
8368
+ "none",
8369
+ "device"
8370
+ ];
8371
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8372
+ new Set(READING_QUANTITIES);
8373
+ /**
8374
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8375
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8376
+ */
8377
+ var QUANTITY_BASE = {
8378
+ temperature: "°C",
8379
+ pressure: "hPa",
8380
+ speed: "m/s",
8381
+ precipitation: "mm",
8382
+ "precipitation-rate": "mm/h",
8383
+ length: "m",
8384
+ area: "m²",
8385
+ illuminance: "lx",
8386
+ irradiance: "W/m²",
8387
+ power: "W",
8388
+ energy: "kWh",
8389
+ voltage: "V",
8390
+ current: "A",
8391
+ "apparent-power": "VA",
8392
+ humidity: "%",
8393
+ percentage: "%",
8394
+ "concentration-volume": "ppm",
8395
+ "concentration-mass": "µg/m³",
8396
+ "concentration-ppb": "ppb",
8397
+ volume: "L",
8398
+ duration: "s",
8399
+ "duration-ms": "ms",
8400
+ information: "B",
8401
+ "sound-level": "dBFS",
8402
+ "signal-level": "dBm",
8403
+ "colour-temperature": "mired",
8404
+ angle: "°",
8405
+ bitrate: "kbps",
8406
+ rate: "fps",
8407
+ mass: "kg",
8408
+ index: "none",
8409
+ ratio: "none",
8410
+ count: "none",
8411
+ none: "none",
8412
+ device: "device"
8413
+ };
8414
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8415
+ function baseUnitOf(quantity) {
8416
+ const symbol = QUANTITY_BASE[quantity];
8417
+ if (symbol === "none" || symbol === "device") return void 0;
8418
+ return symbol;
8419
+ }
8420
+ function readingMeta(quantity, extras) {
8421
+ return {
8422
+ quantity,
8423
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8424
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8425
+ };
8426
+ }
8326
8427
  /** Shared Zod schemas used across streaming capabilities. */
8327
8428
  var CamProfileSchema = _enum([
8328
8429
  "high",
@@ -8350,8 +8451,8 @@ var CamStreamKindSchema = _enum([
8350
8451
  "derived"
8351
8452
  ]);
8352
8453
  var CamStreamResolutionSchema = object({
8353
- width: number().int().positive(),
8354
- height: number().int().positive()
8454
+ width: number().int().positive().meta(readingMeta("none")),
8455
+ height: number().int().positive().meta(readingMeta("none"))
8355
8456
  });
8356
8457
  var CameraStreamSchema = object({
8357
8458
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8869,6 +8970,8 @@ var CAP_NAMES = Object.freeze([
8869
8970
  "filesystem-browse",
8870
8971
  "flood",
8871
8972
  "gas",
8973
+ "group-commands",
8974
+ "group-status",
8872
8975
  "heating-valve",
8873
8976
  "humidifier",
8874
8977
  "humidity-sensor",
@@ -8998,14 +9101,6 @@ Object.freeze({
8998
9101
  {
8999
9102
  "path": "aqi",
9000
9103
  "valueType": "number"
9001
- },
9002
- {
9003
- "path": "unit",
9004
- "valueType": "string"
9005
- },
9006
- {
9007
- "path": "precision",
9008
- "valueType": "number"
9009
9104
  }
9010
9105
  ],
9011
9106
  "alarm-panel": [
@@ -9041,20 +9136,10 @@ Object.freeze({
9041
9136
  "valueType": "boolean"
9042
9137
  }
9043
9138
  ],
9044
- "ambient-light-sensor": [
9045
- {
9046
- "path": "lux",
9047
- "valueType": "number"
9048
- },
9049
- {
9050
- "path": "unit",
9051
- "valueType": "string"
9052
- },
9053
- {
9054
- "path": "precision",
9055
- "valueType": "number"
9056
- }
9057
- ],
9139
+ "ambient-light-sensor": [{
9140
+ "path": "lux",
9141
+ "valueType": "number"
9142
+ }],
9058
9143
  "audio-metrics": [
9059
9144
  {
9060
9145
  "path": "ts",
@@ -9134,6 +9219,10 @@ Object.freeze({
9134
9219
  "path": "lastUpdated",
9135
9220
  "valueType": "number"
9136
9221
  },
9222
+ {
9223
+ "path": "volts",
9224
+ "valueType": "number"
9225
+ },
9137
9226
  {
9138
9227
  "path": "lastContactAt",
9139
9228
  "valueType": "number"
@@ -9376,10 +9465,6 @@ Object.freeze({
9376
9465
  "datetime"
9377
9466
  ]
9378
9467
  },
9379
- {
9380
- "path": "unit",
9381
- "valueType": "string"
9382
- },
9383
9468
  {
9384
9469
  "path": "min",
9385
9470
  "valueType": "number"
@@ -9391,19 +9476,6 @@ Object.freeze({
9391
9476
  {
9392
9477
  "path": "step",
9393
9478
  "valueType": "number"
9394
- },
9395
- {
9396
- "path": "precision",
9397
- "valueType": "number"
9398
- },
9399
- {
9400
- "path": "format",
9401
- "valueType": "enum",
9402
- "enumValues": [
9403
- "date",
9404
- "time",
9405
- "datetime"
9406
- ]
9407
9479
  }
9408
9480
  ],
9409
9481
  "cover": [
@@ -9644,13 +9716,8 @@ Object.freeze({
9644
9716
  "path": "value",
9645
9717
  "valueType": "string"
9646
9718
  }, {
9647
- "path": "format",
9648
- "valueType": "enum",
9649
- "enumValues": [
9650
- "date",
9651
- "time",
9652
- "datetime"
9653
- ]
9719
+ "path": "timestamp",
9720
+ "valueType": "boolean"
9654
9721
  }],
9655
9722
  "event-emitter": [
9656
9723
  {
@@ -9723,6 +9790,20 @@ Object.freeze({
9723
9790
  "path": "detected",
9724
9791
  "valueType": "boolean"
9725
9792
  }],
9793
+ "group-status": [
9794
+ {
9795
+ "path": "included",
9796
+ "valueType": "number"
9797
+ },
9798
+ {
9799
+ "path": "excluded",
9800
+ "valueType": "number"
9801
+ },
9802
+ {
9803
+ "path": "total",
9804
+ "valueType": "number"
9805
+ }
9806
+ ],
9726
9807
  "heating-valve": [{
9727
9808
  "path": "position",
9728
9809
  "valueType": "number"
@@ -9757,20 +9838,10 @@ Object.freeze({
9757
9838
  "valueType": "number"
9758
9839
  }
9759
9840
  ],
9760
- "humidity-sensor": [
9761
- {
9762
- "path": "percent",
9763
- "valueType": "number"
9764
- },
9765
- {
9766
- "path": "unit",
9767
- "valueType": "string"
9768
- },
9769
- {
9770
- "path": "precision",
9771
- "valueType": "number"
9772
- }
9773
- ],
9841
+ "humidity-sensor": [{
9842
+ "path": "percent",
9843
+ "valueType": "number"
9844
+ }],
9774
9845
  "image": [{
9775
9846
  "path": "url",
9776
9847
  "valueType": "string"
@@ -10109,20 +10180,10 @@ Object.freeze({
10109
10180
  "valueType": "boolean"
10110
10181
  }
10111
10182
  ],
10112
- "numeric-sensor": [
10113
- {
10114
- "path": "value",
10115
- "valueType": "number"
10116
- },
10117
- {
10118
- "path": "unit",
10119
- "valueType": "string"
10120
- },
10121
- {
10122
- "path": "precision",
10123
- "valueType": "number"
10124
- }
10125
- ],
10183
+ "numeric-sensor": [{
10184
+ "path": "value",
10185
+ "valueType": "number"
10186
+ }],
10126
10187
  "pet-feeder": [
10127
10188
  {
10128
10189
  "path": "foodLevel",
@@ -10214,14 +10275,6 @@ Object.freeze({
10214
10275
  {
10215
10276
  "path": "powerFactor",
10216
10277
  "valueType": "number"
10217
- },
10218
- {
10219
- "path": "unit",
10220
- "valueType": "string"
10221
- },
10222
- {
10223
- "path": "precision",
10224
- "valueType": "number"
10225
10278
  }
10226
10279
  ],
10227
10280
  "presence": [
@@ -10250,20 +10303,10 @@ Object.freeze({
10250
10303
  "valueType": "number"
10251
10304
  }
10252
10305
  ],
10253
- "pressure-sensor": [
10254
- {
10255
- "path": "hpa",
10256
- "valueType": "number"
10257
- },
10258
- {
10259
- "path": "unit",
10260
- "valueType": "string"
10261
- },
10262
- {
10263
- "path": "precision",
10264
- "valueType": "number"
10265
- }
10266
- ],
10306
+ "pressure-sensor": [{
10307
+ "path": "hpa",
10308
+ "valueType": "number"
10309
+ }],
10267
10310
  "privacy-mask": [{
10268
10311
  "path": "enabled",
10269
10312
  "valueType": "boolean"
@@ -10574,20 +10617,10 @@ Object.freeze({
10574
10617
  "path": "tampered",
10575
10618
  "valueType": "boolean"
10576
10619
  }],
10577
- "temperature-sensor": [
10578
- {
10579
- "path": "celsius",
10580
- "valueType": "number"
10581
- },
10582
- {
10583
- "path": "unit",
10584
- "valueType": "string"
10585
- },
10586
- {
10587
- "path": "precision",
10588
- "valueType": "number"
10589
- }
10590
- ],
10620
+ "temperature-sensor": [{
10621
+ "path": "celsius",
10622
+ "valueType": "number"
10623
+ }],
10591
10624
  "update": [
10592
10625
  {
10593
10626
  "path": "currentVersion",
@@ -10698,10 +10731,6 @@ Object.freeze({
10698
10731
  "path": "temperature",
10699
10732
  "valueType": "number"
10700
10733
  },
10701
- {
10702
- "path": "temperatureUnit",
10703
- "valueType": "string"
10704
- },
10705
10734
  {
10706
10735
  "path": "humidity",
10707
10736
  "valueType": "number"
@@ -10710,18 +10739,10 @@ Object.freeze({
10710
10739
  "path": "pressure",
10711
10740
  "valueType": "number"
10712
10741
  },
10713
- {
10714
- "path": "pressureUnit",
10715
- "valueType": "string"
10716
- },
10717
10742
  {
10718
10743
  "path": "windSpeed",
10719
10744
  "valueType": "number"
10720
10745
  },
10721
- {
10722
- "path": "windSpeedUnit",
10723
- "valueType": "string"
10724
- },
10725
10746
  {
10726
10747
  "path": "windBearing",
10727
10748
  "valueType": "number"
@@ -11124,7 +11145,7 @@ function errMsg(err) {
11124
11145
  return String(err);
11125
11146
  }
11126
11147
  //#endregion
11127
- //#region ../types/dist/composition-kEl9mevu.mjs
11148
+ //#region ../types/dist/composition-OhpuespA.mjs
11128
11149
  /**
11129
11150
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11130
11151
  *
@@ -11879,15 +11900,74 @@ function validateExpressionSource(src) {
11879
11900
  }
11880
11901
  return null;
11881
11902
  }
11882
- /** A composed device's stableId is this prefix plus the block id. */
11883
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11884
- /** Longest snippet a `code` source may carry (slice 4). */
11885
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11903
+ /**
11904
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11905
+ * id. Split out so helper schemas can name it without a cycle through
11906
+ * `composition.ts` (D659).
11907
+ */
11886
11908
  var CompositionSourceRefSchema = object({
11887
11909
  /** The owning addon; `stableId` is unique only within it. */
11888
11910
  addonId: string$2().min(1),
11889
11911
  stableId: string$2().min(1)
11890
11912
  });
11913
+ var GROUP_TEMPLATE_IDS = [
11914
+ "temperature",
11915
+ "humidity",
11916
+ "pressure",
11917
+ "illuminance",
11918
+ "power",
11919
+ "energy",
11920
+ "battery",
11921
+ "numeric",
11922
+ "motion",
11923
+ "contact",
11924
+ "leak",
11925
+ "smoke",
11926
+ "occupancy",
11927
+ "binary",
11928
+ "switch",
11929
+ "light"
11930
+ ];
11931
+ var NUMERIC_AGGREGATES = [
11932
+ "mean",
11933
+ "median",
11934
+ "min",
11935
+ "max",
11936
+ "sum"
11937
+ ];
11938
+ var BINARY_AGGREGATES = ["any", "all"];
11939
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11940
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11941
+ var AGGREGATE_REDUCERS = [
11942
+ "mean",
11943
+ "median",
11944
+ "min",
11945
+ "max",
11946
+ "sum",
11947
+ "any",
11948
+ "all"
11949
+ ];
11950
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11951
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11952
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11953
+ /** One member. The field is stored so occupancy can name motion or binary. */
11954
+ var HelperMemberSchema = object({
11955
+ source: CompositionSourceRefSchema,
11956
+ cap: string$2().min(1),
11957
+ fieldPath: string$2().min(1)
11958
+ });
11959
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11960
+ kind: literal("group"),
11961
+ templateId: GroupTemplateIdSchema,
11962
+ aggregate: AggregateReducerSchema,
11963
+ members: array(HelperMemberSchema).min(1).max(64),
11964
+ /** Actuator groups only. Readings leave this unset. */
11965
+ active: ActiveCriterionSchema.optional()
11966
+ })]);
11967
+ /** A composed device's stableId is this prefix plus the block id. */
11968
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11969
+ /** Longest snippet a `code` source may carry (slice 4). */
11970
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11891
11971
  /** One field of one capability of one source device. */
11892
11972
  var CompositionFieldReadSchema = object({
11893
11973
  source: CompositionSourceRefSchema,
@@ -11931,7 +12011,33 @@ var CompositionCodeSourceSchema = object({
11931
12011
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11932
12012
  CompositionFromSourceSchema,
11933
12013
  CompositionExpressionSourceSchema,
11934
- CompositionCodeSourceSchema
12014
+ CompositionCodeSourceSchema,
12015
+ object({
12016
+ kind: literal("aggregate"),
12017
+ reducer: _enum([
12018
+ "mean",
12019
+ "median",
12020
+ "min",
12021
+ "max",
12022
+ "sum",
12023
+ "any",
12024
+ "all"
12025
+ ]),
12026
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12027
+ /** `value` is the reading; the others are the member counts. */
12028
+ publish: _enum([
12029
+ "value",
12030
+ "included",
12031
+ "excluded",
12032
+ "total"
12033
+ ]),
12034
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12035
+ empty: _enum([
12036
+ "null",
12037
+ "omit",
12038
+ "offline"
12039
+ ])
12040
+ })
11935
12041
  ]);
11936
12042
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11937
12043
  kind: literal("forward"),
@@ -11977,7 +12083,13 @@ var CompositionSchema = object({
11977
12083
  kind: literal("existing"),
11978
12084
  device: CompositionSourceRefSchema
11979
12085
  })]),
11980
- features: array(CompositionFeatureSchema).min(1).max(16)
12086
+ features: array(CompositionFeatureSchema).min(1).max(16),
12087
+ /**
12088
+ * Present when the block is a helper template. Absent on a free
12089
+ * composition (the room example, device 4603). The editor reads this,
12090
+ * never the compiled features.
12091
+ */
12092
+ helper: HelperSpecSchema.optional()
11981
12093
  }).superRefine((composition, ctx) => {
11982
12094
  const seen = /* @__PURE__ */ new Set();
11983
12095
  composition.features.forEach((feature, index) => {
@@ -18234,7 +18346,11 @@ var CompositionProblemSchema = object({
18234
18346
  "item-field-reserved",
18235
18347
  "target-missing",
18236
18348
  "target-unreadable",
18237
- "not-replaceable"
18349
+ "not-replaceable",
18350
+ "native-capability-replace",
18351
+ "mixed-quantity",
18352
+ "helper-mismatch",
18353
+ "member-constraint"
18238
18354
  ]),
18239
18355
  path: string$2(),
18240
18356
  message: string$2()
@@ -18255,7 +18371,8 @@ var CompositionUnavailableModeSchema = _enum([
18255
18371
  "null",
18256
18372
  "offline",
18257
18373
  "unknown",
18258
- "release"
18374
+ "release",
18375
+ "omit"
18259
18376
  ]);
18260
18377
  /**
18261
18378
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18292,9 +18409,11 @@ var CompositionFieldPlanSchema = object({
18292
18409
  reads: array(CompositionFieldReadSchema)
18293
18410
  });
18294
18411
  /**
18295
- * What a feature does to its target (D663): `new` builds a capability on a new
18296
- * device; on an existing device it `add`s a capability the device lacks, or
18297
- * `replace`s named fields of one it has natively.
18412
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18413
+ * new device; on an existing device it `add`s a capability the device lacks.
18414
+ * Replacing a capability the device already serves is refused
18415
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18416
+ * wire for a claim the hub already holds.
18298
18417
  */
18299
18418
  var CompositionFeatureModeSchema = _enum([
18300
18419
  "new",
@@ -18467,6 +18586,275 @@ Object.freeze({
18467
18586
  result: ["number", "null"]
18468
18587
  }
18469
18588
  });
18589
+ /**
18590
+ * One catalog row per base reading a group helper can build (D944 §3).
18591
+ * Data, not a form default written into the block.
18592
+ */
18593
+ var NOW = {
18594
+ fieldPath: "lastFetchedAt",
18595
+ expr: "now"
18596
+ };
18597
+ var CHANGED = {
18598
+ fieldPath: "lastChangedAt",
18599
+ expr: "now"
18600
+ };
18601
+ function numeric(input) {
18602
+ return {
18603
+ ...input,
18604
+ family: "numeric",
18605
+ deviceType: DeviceType.Sensor,
18606
+ memberDeviceType: DeviceType.Sensor,
18607
+ commandCaps: [],
18608
+ baseUnit: baseUnitOf(input.quantity),
18609
+ aggregates: NUMERIC_AGGREGATES,
18610
+ accept: [{
18611
+ cap: input.outputCap,
18612
+ fieldPath: input.outputField
18613
+ }]
18614
+ };
18615
+ }
18616
+ function binary(input) {
18617
+ return {
18618
+ ...input,
18619
+ family: "binary",
18620
+ deviceType: DeviceType.Sensor,
18621
+ memberDeviceType: DeviceType.Sensor,
18622
+ commandCaps: [],
18623
+ quantity: "none",
18624
+ baseUnit: void 0,
18625
+ aggregates: BINARY_AGGREGATES,
18626
+ empty: "offline"
18627
+ };
18628
+ }
18629
+ var GROUP_TEMPLATES = [
18630
+ numeric({
18631
+ id: "temperature",
18632
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18633
+ role: DeviceRole.TemperatureSensor,
18634
+ outputCap: "temperature-sensor",
18635
+ outputField: "celsius",
18636
+ quantity: "temperature",
18637
+ defaultAggregate: "mean",
18638
+ empty: "null",
18639
+ companions: [NOW]
18640
+ }),
18641
+ numeric({
18642
+ id: "humidity",
18643
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18644
+ role: DeviceRole.HumiditySensor,
18645
+ outputCap: "humidity-sensor",
18646
+ outputField: "percent",
18647
+ quantity: "humidity",
18648
+ defaultAggregate: "mean",
18649
+ empty: "null",
18650
+ companions: [NOW]
18651
+ }),
18652
+ numeric({
18653
+ id: "pressure",
18654
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18655
+ role: DeviceRole.PressureSensor,
18656
+ outputCap: "pressure-sensor",
18657
+ outputField: "hpa",
18658
+ quantity: "pressure",
18659
+ defaultAggregate: "mean",
18660
+ empty: "null",
18661
+ companions: [NOW]
18662
+ }),
18663
+ numeric({
18664
+ id: "illuminance",
18665
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18666
+ role: DeviceRole.AmbientLightSensor,
18667
+ outputCap: "ambient-light-sensor",
18668
+ outputField: "lux",
18669
+ quantity: "illuminance",
18670
+ defaultAggregate: "mean",
18671
+ empty: "null",
18672
+ companions: [NOW]
18673
+ }),
18674
+ numeric({
18675
+ id: "power",
18676
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18677
+ role: DeviceRole.PowerSensor,
18678
+ outputCap: "power-meter",
18679
+ outputField: "watts",
18680
+ quantity: "power",
18681
+ defaultAggregate: "sum",
18682
+ empty: "omit",
18683
+ companions: [NOW]
18684
+ }),
18685
+ numeric({
18686
+ id: "energy",
18687
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18688
+ role: DeviceRole.EnergySensor,
18689
+ outputCap: "power-meter",
18690
+ outputField: "kwhTotal",
18691
+ quantity: "energy",
18692
+ defaultAggregate: "sum",
18693
+ empty: "omit",
18694
+ companions: [NOW]
18695
+ }),
18696
+ numeric({
18697
+ id: "battery",
18698
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18699
+ role: DeviceRole.BatterySensor,
18700
+ outputCap: "battery",
18701
+ outputField: "percentage",
18702
+ quantity: "percentage",
18703
+ defaultAggregate: "mean",
18704
+ empty: "null",
18705
+ companions: [
18706
+ {
18707
+ fieldPath: "charging",
18708
+ expr: "\"unknown\""
18709
+ },
18710
+ {
18711
+ fieldPath: "sleeping",
18712
+ expr: "false"
18713
+ },
18714
+ {
18715
+ fieldPath: "lastUpdated",
18716
+ expr: "now"
18717
+ }
18718
+ ]
18719
+ }),
18720
+ numeric({
18721
+ id: "numeric",
18722
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18723
+ role: DeviceRole.NumericSensor,
18724
+ outputCap: "numeric-sensor",
18725
+ outputField: "value",
18726
+ quantity: "device",
18727
+ defaultAggregate: "mean",
18728
+ empty: "null",
18729
+ companions: [NOW]
18730
+ }),
18731
+ binary({
18732
+ id: "motion",
18733
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18734
+ role: DeviceRole.MotionSensor,
18735
+ outputCap: "motion",
18736
+ outputField: "detected",
18737
+ defaultAggregate: "any",
18738
+ accept: [{
18739
+ cap: "motion",
18740
+ fieldPath: "detected"
18741
+ }],
18742
+ companions: [{
18743
+ fieldPath: "lastDetectedAt",
18744
+ expr: "null"
18745
+ }, {
18746
+ fieldPath: "autoClearAfterMs",
18747
+ expr: "null"
18748
+ }]
18749
+ }),
18750
+ binary({
18751
+ id: "contact",
18752
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18753
+ role: DeviceRole.ContactSensor,
18754
+ outputCap: "contact",
18755
+ outputField: "entryOpen",
18756
+ defaultAggregate: "any",
18757
+ accept: [{
18758
+ cap: "contact",
18759
+ fieldPath: "entryOpen"
18760
+ }],
18761
+ companions: [CHANGED]
18762
+ }),
18763
+ binary({
18764
+ id: "leak",
18765
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18766
+ role: DeviceRole.LeakSensor,
18767
+ outputCap: "flood",
18768
+ outputField: "flooded",
18769
+ defaultAggregate: "any",
18770
+ accept: [{
18771
+ cap: "flood",
18772
+ fieldPath: "flooded"
18773
+ }],
18774
+ companions: [CHANGED]
18775
+ }),
18776
+ binary({
18777
+ id: "smoke",
18778
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18779
+ role: DeviceRole.SmokeSensor,
18780
+ outputCap: "smoke",
18781
+ outputField: "detected",
18782
+ defaultAggregate: "any",
18783
+ accept: [{
18784
+ cap: "smoke",
18785
+ fieldPath: "detected"
18786
+ }],
18787
+ companions: [CHANGED]
18788
+ }),
18789
+ binary({
18790
+ id: "occupancy",
18791
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18792
+ role: DeviceRole.BinarySensor,
18793
+ outputCap: "binary",
18794
+ outputField: "on",
18795
+ defaultAggregate: "any",
18796
+ accept: [{
18797
+ cap: "motion",
18798
+ fieldPath: "detected"
18799
+ }, {
18800
+ cap: "binary",
18801
+ fieldPath: "on"
18802
+ }],
18803
+ companions: [CHANGED]
18804
+ }),
18805
+ binary({
18806
+ id: "binary",
18807
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18808
+ role: DeviceRole.BinarySensor,
18809
+ outputCap: "binary",
18810
+ outputField: "on",
18811
+ defaultAggregate: "any",
18812
+ accept: [{
18813
+ cap: "binary",
18814
+ fieldPath: "on"
18815
+ }],
18816
+ companions: [CHANGED]
18817
+ }),
18818
+ actuator({
18819
+ id: "switch",
18820
+ 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.",
18821
+ deviceType: DeviceType.Switch,
18822
+ outputCap: "switch",
18823
+ commandCaps: ["switch"]
18824
+ }),
18825
+ actuator({
18826
+ id: "light",
18827
+ 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.",
18828
+ deviceType: DeviceType.Light,
18829
+ outputCap: "switch",
18830
+ commandCaps: [
18831
+ "switch",
18832
+ "brightness",
18833
+ "color"
18834
+ ]
18835
+ })
18836
+ ];
18837
+ function actuator(input) {
18838
+ return {
18839
+ ...input,
18840
+ family: "actuator",
18841
+ role: void 0,
18842
+ memberDeviceType: input.deviceType,
18843
+ outputField: "on",
18844
+ quantity: "none",
18845
+ baseUnit: void 0,
18846
+ aggregates: BINARY_AGGREGATES,
18847
+ defaultAggregate: "any",
18848
+ empty: "offline",
18849
+ accept: [{
18850
+ cap: "switch",
18851
+ fieldPath: "on"
18852
+ }],
18853
+ companions: [CHANGED]
18854
+ };
18855
+ }
18856
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18857
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18470
18858
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18471
18859
  /**
18472
18860
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20180,12 +20568,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20180
20568
  * providers detect renames by matching `uniqueId` and patch `id`
20181
20569
  * in-place so dispatch keeps working.
20182
20570
  *
20183
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20184
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20185
- * state push and are the single source of truth. The `device_class` tag is
20186
- * an HA-specific input that is consumed at the discovery boundary
20187
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20188
- * stored after that point.
20571
+ * **Display unit and precision are NOT stored here.** They are a read-time
20572
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20573
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20574
+ * neither is repeated on each sample. The `device_class` tag is an
20575
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20576
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20189
20577
  *
20190
20578
  * The `raw` escape hatch carries provider-specific attributes that don't
20191
20579
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20199,6 +20587,14 @@ var SourceInfoSchema = object({
20199
20587
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20200
20588
  * Used to detect rename when `id` changes. */
20201
20589
  uniqueId: string$2().optional(),
20590
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20591
+ quantity: ReadingQuantitySchema.optional(),
20592
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20593
+ dateTimeShape: _enum([
20594
+ "date",
20595
+ "time",
20596
+ "datetime"
20597
+ ]).optional(),
20202
20598
  /** Provider-specific extras that don't fit the structured slots.
20203
20599
  * Whitelist what goes here per provider — do NOT dump entire HA
20204
20600
  * attribute blobs (would explode the meta JSON column). */
@@ -41320,7 +41716,8 @@ method(object({
41320
41716
  "enum"
41321
41717
  ]),
41322
41718
  enumValues: array(string$2()).optional(),
41323
- item: boolean().optional()
41719
+ item: boolean().optional(),
41720
+ quantity: ReadingQuantitySchema.optional()
41324
41721
  })).readonly(),
41325
41722
  itemArray: object({
41326
41723
  path: string$2(),
@@ -45164,8 +45561,8 @@ var MaskPolygonShapeSchema = object({
45164
45561
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45165
45562
  var MaskGridShapeSchema = object({
45166
45563
  kind: literal("grid"),
45167
- gridWidth: number(),
45168
- gridHeight: number(),
45564
+ gridWidth: number().meta(readingMeta("none")),
45565
+ gridHeight: number().meta(readingMeta("none")),
45169
45566
  cells: array(boolean())
45170
45567
  });
45171
45568
  discriminatedUnion("kind", [
@@ -45277,6 +45674,8 @@ var NcSystemEventKindSchema = _enum([
45277
45674
  "detection-blind",
45278
45675
  "motion-noisy",
45279
45676
  "motion-calm",
45677
+ "package-llm-silent",
45678
+ "package-llm-answered",
45280
45679
  "recording-stalled",
45281
45680
  "recording-healthy",
45282
45681
  "recording-degraded",
@@ -56368,6 +56767,28 @@ var PackageVersionInfoSchema = object({
56368
56767
  deprecated: string$2().optional(),
56369
56768
  distTags: array(string$2()).readonly()
56370
56769
  });
56770
+ /**
56771
+ * One published document. `ok` is markdown from the registry. `missing` means
56772
+ * the registry answered and the file was not there. `error` is a registry or
56773
+ * tarball failure — never an empty string standing in for either.
56774
+ */
56775
+ var PackageDocBodySchema = discriminatedUnion("status", [
56776
+ object({
56777
+ status: literal("ok"),
56778
+ markdown: string$2()
56779
+ }),
56780
+ object({ status: literal("missing") }),
56781
+ object({
56782
+ status: literal("error"),
56783
+ message: string$2()
56784
+ })
56785
+ ]);
56786
+ var PackageDocsSchema = object({
56787
+ name: string$2(),
56788
+ version: string$2(),
56789
+ readme: PackageDocBodySchema,
56790
+ changelog: PackageDocBodySchema
56791
+ });
56371
56792
  var SearchResultSchema = object({
56372
56793
  name: string$2(),
56373
56794
  version: string$2(),
@@ -56586,7 +57007,10 @@ method(object({ nodeId: string$2().optional() }).optional(), array(AddonListItem
56586
57007
  }), object({ success: literal(true) }), {
56587
57008
  kind: "mutation",
56588
57009
  auth: "admin"
56589
- }), method(object({ name: string$2() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string$2() }), RestartAddonResultSchema, {
57010
+ }), method(object({ name: string$2() }), array(PackageVersionInfoSchema).readonly()), method(object({
57011
+ name: string$2().min(1),
57012
+ version: string$2().min(1).optional()
57013
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string$2() }), RestartAddonResultSchema, {
56590
57014
  kind: "mutation",
56591
57015
  auth: "admin"
56592
57016
  }), method(object({ packageName: string$2() }), object({ success: literal(true) }), {
@@ -56645,27 +57069,17 @@ method(object({ nodeId: string$2().optional() }).optional(), array(AddonListItem
56645
57069
  */
56646
57070
  var AirQualitySensorStatusSchema = object({
56647
57071
  /** Carbon dioxide concentration in ppm. */
56648
- co2Ppm: number().min(0).optional(),
57072
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56649
57073
  /** Total volatile organic compounds in ppb. */
56650
- vocPpb: number().min(0).optional(),
57074
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56651
57075
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56652
- pm25: number().min(0).optional(),
57076
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56653
57077
  /** Particulate matter ≤ 10 μm in µg/m³. */
56654
- pm10: number().min(0).optional(),
57078
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56655
57079
  /** Composite AQI value (typically 0..500). */
56656
- aqi: number().optional(),
57080
+ aqi: number().optional().meta(readingMeta("index")),
56657
57081
  /** Ms epoch when the slice was last updated. */
56658
- lastFetchedAt: number(),
56659
- /** Live display unit of the single metric this slice carries (e.g. HA
56660
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56661
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56662
- * per slice is unambiguous. */
56663
- unit: string$2().optional(),
56664
- /** Suggested decimal places for numeric display.
56665
- * Populated live from the upstream source when provided (e.g. HA
56666
- * `attributes.suggested_display_precision`). Falls back to
56667
- * auto-formatting when absent. */
56668
- precision: number().int().min(0).max(10).optional()
57082
+ lastFetchedAt: number()
56669
57083
  });
56670
57084
  var airQualitySensorCapability = {
56671
57085
  name: "air-quality-sensor",
@@ -56702,12 +57116,12 @@ var airQualitySensorCapability = {
56702
57116
  },
56703
57117
  pm25: {
56704
57118
  kind: "gauge",
56705
- unit: "ug/m3",
57119
+ unit: "µg/m³",
56706
57120
  deadband: 1
56707
57121
  },
56708
57122
  pm10: {
56709
57123
  kind: "gauge",
56710
- unit: "ug/m3",
57124
+ unit: "µg/m³",
56711
57125
  deadband: 1
56712
57126
  },
56713
57127
  aqi: {
@@ -56725,22 +57139,13 @@ var airQualitySensorCapability = {
56725
57139
  * entries with `device_class: illuminance`.
56726
57140
  */
56727
57141
  var AmbientLightSensorStatusSchema = object({
56728
- /** Current illuminance in lux (lx). */
56729
- lux: number().min(0).meta({
56730
- unit: "lx",
57142
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57143
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57144
+ precision: 0,
56731
57145
  label: "Illuminance"
56732
- }),
57146
+ })),
56733
57147
  /** Ms epoch when the slice was last updated. */
56734
- lastFetchedAt: number(),
56735
- /** Live display unit from the upstream source (e.g. HA
56736
- * `attributes.unit_of_measurement`). The UI prefers this over the
56737
- * role's canonical unit. Absent → fall back to the canonical unit. */
56738
- unit: string$2().optional(),
56739
- /** Suggested decimal places for numeric display.
56740
- * Populated live from the upstream source when provided (e.g. HA
56741
- * `attributes.suggested_display_precision`). Falls back to
56742
- * auto-formatting when absent. */
56743
- precision: number().int().min(0).max(10).optional()
57148
+ lastFetchedAt: number()
56744
57149
  });
56745
57150
  var ambientLightSensorCapability = {
56746
57151
  name: "ambient-light-sensor",
@@ -56772,12 +57177,7 @@ var ambientLightSensorCapability = {
56772
57177
  } } },
56773
57178
  /** Clock fields: written, but excluded from the compare that decides
56774
57179
  * whether persisting is worth a SQLite commit. */
56775
- volatileStateFields: ["lastFetchedAt"],
56776
- customize: {
56777
- version: 1,
56778
- advanced: "derive",
56779
- targets: []
56780
- }
57180
+ volatileStateFields: ["lastFetchedAt"]
56781
57181
  };
56782
57182
  /**
56783
57183
  * Per-class audio metrics aggregated over a sliding window.
@@ -57004,10 +57404,7 @@ var BatteryStatusSchema = object({
57004
57404
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57005
57405
  * declines to band a non-finite reading, which is the correct shape.
57006
57406
  */
57007
- percentage: number().min(0).max(100).nullable().meta({
57008
- unit: "%",
57009
- label: "Charge"
57010
- }),
57407
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57011
57408
  /**
57012
57409
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57013
57410
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57034,7 +57431,14 @@ var BatteryStatusSchema = object({
57034
57431
  */
57035
57432
  sleeping: boolean().meta({ label: "Sleeping" }),
57036
57433
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57037
- lastUpdated: number(),
57434
+ lastUpdated: number().meta(readingMeta("none")),
57435
+ /**
57436
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57437
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57438
+ * that only report charge. A second reading on this cap, not a second
57439
+ * `numeric-sensor` on the same device.
57440
+ */
57441
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57038
57442
  /**
57039
57443
  * Ms epoch of the last time the device PROVED it was reachable — a
57040
57444
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57086,7 +57490,6 @@ var BATTERY_DEVICE_TYPES = [
57086
57490
  /** Generic Customize target for the percentage reading (D936). */
57087
57491
  var batteryCustomize = {
57088
57492
  version: 1,
57089
- advanced: "derive",
57090
57493
  targets: [{
57091
57494
  id: "percentage",
57092
57495
  cardinality: "scalar",
@@ -57095,7 +57498,7 @@ var batteryCustomize = {
57095
57498
  stateField: "percentage"
57096
57499
  },
57097
57500
  deviceTypes: BATTERY_DEVICE_TYPES,
57098
- apply: "add-or-replace",
57501
+ apply: "add",
57099
57502
  path: "percentage",
57100
57503
  sources: [{
57101
57504
  kind: "field",
@@ -57216,10 +57619,16 @@ onStatusChanged: { data: object({
57216
57619
  /**
57217
57620
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57218
57621
  */
57219
- history: { fields: { percentage: {
57220
- kind: "gauge",
57221
- unit: "%"
57222
- } } },
57622
+ history: { fields: {
57623
+ percentage: {
57624
+ kind: "gauge",
57625
+ unit: "%"
57626
+ },
57627
+ volts: {
57628
+ kind: "gauge",
57629
+ unit: "V"
57630
+ }
57631
+ } },
57223
57632
  customize: batteryCustomize,
57224
57633
  /** Clock fields: written, but excluded from the compare that decides
57225
57634
  * whether persisting is worth a SQLite commit. */
@@ -57266,12 +57675,7 @@ var binaryCapability = {
57266
57675
  history: { fields: { on: { kind: "boolean" } } },
57267
57676
  /** Clock fields: written, but excluded from the compare that decides
57268
57677
  * whether persisting is worth a SQLite commit. */
57269
- volatileStateFields: ["lastChangedAt"],
57270
- customize: {
57271
- version: 1,
57272
- advanced: "derive",
57273
- targets: []
57274
- }
57678
+ volatileStateFields: ["lastChangedAt"]
57275
57679
  };
57276
57680
  /**
57277
57681
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57287,7 +57691,7 @@ var binaryCapability = {
57287
57691
  */
57288
57692
  var BrightnessStatusSchema = object({
57289
57693
  /** Current level as 0..100 inclusive. Firmware-reported. */
57290
- percentage: number().min(0).max(100),
57694
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57291
57695
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57292
57696
  lastChangedAt: number()
57293
57697
  });
@@ -57758,21 +58162,21 @@ var ClimateControlStatusSchema = object({
57758
58162
  /** Single setpoint in Celsius. Used by single-target modes
57759
58163
  * (heat / cool / dry). Null when the device is in a range mode
57760
58164
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57761
- target: number().nullable(),
58165
+ target: number().nullable().meta(readingMeta("temperature")),
57762
58166
  /** Upper bound of the dual setpoint in Celsius. Populated only
57763
58167
  * in `heat_cool` mode. */
57764
- targetHigh: number().nullable(),
58168
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57765
58169
  /** Lower bound of the dual setpoint in Celsius. Populated only
57766
58170
  * in `heat_cool` mode. */
57767
- targetLow: number().nullable(),
58171
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57768
58172
  /** Target relative humidity (0..100). Null when the device has no
57769
58173
  * humidity control. */
57770
- targetHumidity: number().min(0).max(100).nullable(),
58174
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57771
58175
  /** Current measured temperature in Celsius. Read-only — pushed by
57772
58176
  * the device's internal sensor. */
57773
- currentTemp: number().nullable(),
58177
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57774
58178
  /** Current measured relative humidity (0..100). Read-only. */
57775
- currentHumidity: number().min(0).max(100).nullable(),
58179
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57776
58180
  /** Vertical louver swing on/off. Null when the device has no vertical
57777
58181
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57778
58182
  swingVertical: boolean().nullable(),
@@ -57780,11 +58184,11 @@ var ClimateControlStatusSchema = object({
57780
58184
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57781
58185
  swingHorizontal: boolean().nullable(),
57782
58186
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57783
- minTemp: number().nullable().optional(),
58187
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57784
58188
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57785
- maxTemp: number().nullable().optional(),
58189
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57786
58190
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57787
- tempStep: number().positive().nullable().optional(),
58191
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57788
58192
  /** Ms epoch when the slice was last updated (push or command). */
57789
58193
  lastFetchedAt: number()
57790
58194
  });
@@ -57926,17 +58330,17 @@ var climateControlCapability = {
57926
58330
  * modes to expose in the picker without a slice fetch.
57927
58331
  */
57928
58332
  var RgbTripletSchema = object({
57929
- r: number().int().min(0).max(255),
57930
- g: number().int().min(0).max(255),
57931
- b: number().int().min(0).max(255)
58333
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58334
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58335
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57932
58336
  });
57933
58337
  var HsvTripletSchema = object({
57934
58338
  /** Hue in degrees, 0..360. */
57935
- h: number().min(0).max(360),
58339
+ h: number().min(0).max(360).meta(readingMeta("none")),
57936
58340
  /** Saturation as 0..100 inclusive. */
57937
- s: number().min(0).max(100),
58341
+ s: number().min(0).max(100).meta(readingMeta("none")),
57938
58342
  /** Value/brightness as 0..100 inclusive. */
57939
- v: number().min(0).max(100)
58343
+ v: number().min(0).max(100).meta(readingMeta("none"))
57940
58344
  });
57941
58345
  var ColorInputSchema = discriminatedUnion("mode", [
57942
58346
  object({
@@ -57965,7 +58369,7 @@ var ColorStatusSchema = object({
57965
58369
  /** Populated when `mode === 'hsv'`. */
57966
58370
  hsv: HsvTripletSchema.optional(),
57967
58371
  /** Populated when `mode === 'mired'`. */
57968
- mireds: number().int().optional(),
58372
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57969
58373
  /** Ms epoch of the last operator-driven change. */
57970
58374
  lastChangedAt: number()
57971
58375
  });
@@ -58160,18 +58564,12 @@ _enum([
58160
58564
  "empty",
58161
58565
  "full"
58162
58566
  ]);
58163
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58164
- * each. */
58165
- var ConsumableRemainingUnitSchema = _enum([
58166
- "days",
58167
- "hours",
58168
- "cycles"
58169
- ]);
58170
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58171
- * days). `value` null = unknown (D393). */
58567
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58568
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58569
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58172
58570
  var ConsumableRemainingSchema = object({
58173
- value: number().min(0).nullable(),
58174
- unit: ConsumableRemainingUnitSchema
58571
+ value: number().min(0).nullable().meta(readingMeta("device")),
58572
+ kind: ConsumableRemainingKindSchema
58175
58573
  });
58176
58574
  /** A single consumable item. Either a continuous `level` (remaining
58177
58575
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58184,7 +58582,7 @@ var ConsumableItemSchema = object({
58184
58582
  /** Display name. */
58185
58583
  label: string$2().min(1),
58186
58584
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58187
- level: number().min(0).max(100).nullable(),
58585
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58188
58586
  /** The provider's discrete verdict when known (binary mode). */
58189
58587
  status: ConsumableStatusSchema.nullable(),
58190
58588
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58266,8 +58664,8 @@ var consumablesCustomize = {
58266
58664
  transforms: [{ kind: "identity" }]
58267
58665
  }],
58268
58666
  literals: [{
58269
- path: "remaining.unit",
58270
- values: [...ConsumableRemainingUnitSchema.options]
58667
+ path: "remaining.kind",
58668
+ values: [...ConsumableRemainingKindSchema.options]
58271
58669
  }]
58272
58670
  },
58273
58671
  {
@@ -58396,7 +58794,6 @@ reset: method(object({
58396
58794
  },
58397
58795
  "remaining.value": {
58398
58796
  kind: "gauge",
58399
- unitFrom: "remaining.unit",
58400
58797
  deadband: 1,
58401
58798
  displayName: "Remaining"
58402
58799
  }
@@ -58466,10 +58863,9 @@ var contactCapability = {
58466
58863
  * (empty array for the other kinds). It lives in the slice because
58467
58864
  * HA `select.*` entities may update their options at runtime.
58468
58865
  *
58469
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58470
- * `kind === 'numeric'` and are absent (undefined) for all other
58471
- * kinds. They are populated live from HA attributes on each state
58472
- * push — consistent with `options` and the sensor-unit approach.
58866
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58867
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58868
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58473
58869
  */
58474
58870
  var ControlStatusSchema = object({
58475
58871
  kind: _enum([
@@ -58481,39 +58877,17 @@ var ControlStatusSchema = object({
58481
58877
  /** Current value. Numeric kind → number; select/text/datetime → string.
58482
58878
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58483
58879
  * or time-only — HA accepts all three forms). */
58484
- value: union([number(), string$2()]),
58880
+ value: union([number().meta(readingMeta("device")), string$2()]),
58485
58881
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58486
58882
  options: array(string$2()),
58487
58883
  /** Ms epoch when the slice was last updated. */
58488
58884
  lastChangedAt: number(),
58489
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58490
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58491
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58492
- unit: string$2().optional(),
58493
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58494
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58495
- min: number().optional(),
58496
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58497
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58498
- max: number().optional(),
58499
- /** Step increment. Populated live from HA `attributes.step`.
58500
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58501
- step: number().optional(),
58502
- /** Suggested decimal places for numeric display. Populated live from HA
58503
- * `attributes.suggested_display_precision`, or derived from `step` when the
58504
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58505
- * other kinds. Falls back to auto-formatting when absent. */
58506
- precision: number().int().min(0).max(10).optional(),
58507
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58508
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58509
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58510
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58511
- * `has_date`/`has_time` attributes on each push. */
58512
- format: _enum([
58513
- "date",
58514
- "time",
58515
- "datetime"
58516
- ]).optional()
58885
+ /** Minimum allowed value, in the device quantity's base unit. */
58886
+ min: number().optional().meta(readingMeta("device")),
58887
+ /** Maximum allowed value, in the device quantity's base unit. */
58888
+ max: number().optional().meta(readingMeta("device")),
58889
+ /** Step increment, in the device quantity's base unit. */
58890
+ step: number().optional().meta(readingMeta("device"))
58517
58891
  });
58518
58892
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58519
58893
  object({
@@ -58579,9 +58953,9 @@ var CoverStatusSchema = object({
58579
58953
  ]),
58580
58954
  /** 0 = fully closed, 100 = fully open. Null when the device has
58581
58955
  * no intermediate position surface. */
58582
- position: number().min(0).max(100).nullable(),
58956
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58583
58957
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58584
- tiltPosition: number().min(0).max(100).nullable(),
58958
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58585
58959
  /** Ms epoch when the slice was last updated. */
58586
58960
  lastChangedAt: number()
58587
58961
  });
@@ -58684,9 +59058,9 @@ var NormalizedRangeSchema$1 = object({
58684
59058
  var DayNightStatusSchema = object({
58685
59059
  mode: DayNightModeSchema,
58686
59060
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58687
- sensitivity: number().optional(),
59061
+ sensitivity: number().optional().meta(readingMeta("none")),
58688
59062
  /** Delay before the IR-cut filter flips, in seconds. */
58689
- switchDelaySec: number().optional(),
59063
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58690
59064
  lastFetchedAt: number()
58691
59065
  });
58692
59066
  /**
@@ -58904,11 +59278,11 @@ var DishwasherStatusSchema = object({
58904
59278
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58905
59279
  availablePrograms: array(string$2()),
58906
59280
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58907
- remainingSeconds: number().int().nonnegative().nullable(),
59281
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58908
59282
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58909
- delayedStartSeconds: number().int().nonnegative().nullable(),
59283
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58910
59284
  /** Cycle completion 0..100, or null when not applicable. */
58911
- progressPercent: number().min(0).max(100).nullable(),
59285
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58912
59286
  /** True when the door is reported open; null when the door state is not known. */
58913
59287
  doorOpen: boolean().nullable(),
58914
59288
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58932,7 +59306,7 @@ var DishwasherStatusSchema = object({
58932
59306
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58933
59307
  machineCareDue: boolean().nullable(),
58934
59308
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58935
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59309
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58936
59310
  /** Option states: null when the appliance does not expose the option or has not said. */
58937
59311
  halfLoad: boolean().nullable(),
58938
59312
  hygienePlus: boolean().nullable(),
@@ -58947,7 +59321,7 @@ var DishwasherStatusSchema = object({
58947
59321
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58948
59322
  activeEvents: array(string$2()),
58949
59323
  /** Numeric error code when the appliance reports one; null otherwise. */
58950
- errorCode: number().nullable(),
59324
+ errorCode: number().nullable().meta(readingMeta("none")),
58951
59325
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58952
59326
  errorLabel: string$2().nullable(),
58953
59327
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59178,7 +59552,7 @@ var DoorbellStatusSchema = object({
59178
59552
  /** Ms epoch of the last press. null = never observed since this provider started. */
59179
59553
  lastPressedAt: number().nullable(),
59180
59554
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59181
- pressCountSinceStart: number()
59555
+ pressCountSinceStart: number().meta(readingMeta("count"))
59182
59556
  });
59183
59557
  var DoorbellPressEventSchema = object({
59184
59558
  deviceId: number(),
@@ -59243,17 +59617,7 @@ onPressed: {
59243
59617
  history: "none",
59244
59618
  customize: doorbellCustomize
59245
59619
  };
59246
- /**
59247
- * Enum-state sensor — a string value picked from a finite option set.
59248
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59249
- * states, weather conditions, contact-source channels, mode strings).
59250
- * The option set is stable across observations; it is persisted in the
59251
- * device config blob (via the control slice at adoption) and is NOT
59252
- * stored in `sourceInfo`.
59253
- */
59254
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59255
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59256
- var EnumSensorDateTimeFormatSchema = _enum([
59620
+ _enum([
59257
59621
  "date",
59258
59622
  "time",
59259
59623
  "datetime"
@@ -59261,12 +59625,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59261
59625
  var EnumSensorStatusSchema = object({
59262
59626
  value: string$2(),
59263
59627
  /**
59264
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59265
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59266
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59267
- * a time-only sensor → `'time'`.
59628
+ * Set when `value` is a timestamp rather than an enum state, so history
59629
+ * does not learn ISO strings as states. The display shape (date / time /
59630
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59268
59631
  */
59269
- format: EnumSensorDateTimeFormatSchema.optional(),
59632
+ timestamp: boolean().optional(),
59270
59633
  /** Ms epoch when the slice was last updated. */
59271
59634
  lastFetchedAt: number()
59272
59635
  });
@@ -59293,11 +59656,11 @@ var enumSensorCapability = {
59293
59656
  * History (D850): the value as an `enum` series (an appliance's program or
59294
59657
  * operation state, an HVAC action). The option set is per device, so the
59295
59658
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59296
- * `format` carries a timestamp, not a state, and is not recorded.
59659
+ * `timestamp` carries a clock, not a state, and is not recorded.
59297
59660
  */
59298
59661
  history: { fields: { value: {
59299
59662
  kind: "enum",
59300
- skipWhenSet: "format"
59663
+ skipWhenSet: "timestamp"
59301
59664
  } } },
59302
59665
  /** Clock fields: written, but excluded from the compare that decides
59303
59666
  * whether persisting is worth a SQLite commit. */
@@ -59317,13 +59680,13 @@ var EventFireSchema = object({
59317
59680
  deviceId: number(),
59318
59681
  eventType: string$2(),
59319
59682
  data: record(string$2(), unknown()).nullable(),
59320
- timestamp: number(),
59321
- seq: number()
59683
+ timestamp: number().meta(readingMeta("none")),
59684
+ seq: number().meta(readingMeta("count"))
59322
59685
  });
59323
59686
  var EventEmitterStatusSchema = object({
59324
59687
  eventTypes: array(string$2()),
59325
59688
  lastEvent: EventFireSchema.nullable(),
59326
- eventCountSinceStart: number()
59689
+ eventCountSinceStart: number().meta(readingMeta("count"))
59327
59690
  });
59328
59691
  var eventEmitterCapability = {
59329
59692
  name: "event-emitter",
@@ -59736,10 +60099,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59736
60099
  var FanControlStatusSchema = object({
59737
60100
  /** Active speed as 0..100 inclusive. Null when the device has no
59738
60101
  * speed surface (single-speed fan). */
59739
- percentage: number().min(0).max(100).nullable(),
60102
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59740
60103
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59741
60104
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59742
- percentageStep: number().positive().optional(),
60105
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59743
60106
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59744
60107
  * — empty string when no preset is active or supported. */
59745
60108
  preset: string$2(),
@@ -59860,7 +60223,7 @@ var FeatureProbeStatusSchema = object({
59860
60223
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59861
60224
  model: string$2().nullable(),
59862
60225
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59863
- channelCount: number().nullable(),
60226
+ channelCount: number().nullable().meta(readingMeta("count")),
59864
60227
  /**
59865
60228
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59866
60229
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59983,9 +60346,91 @@ var gasCapability = {
59983
60346
  * whether persisting is worth a SQLite commit. */
59984
60347
  volatileStateFields: ["lastChangedAt"]
59985
60348
  };
60349
+ var GroupCommandsSchema = object({
60350
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60351
+ results: array(object({
60352
+ deviceId: number().int().nonnegative(),
60353
+ cap: string$2().min(1),
60354
+ method: string$2().min(1),
60355
+ ok: boolean(),
60356
+ error: string$2().optional(),
60357
+ /** Ms epoch of the attempt. */
60358
+ at: number()
60359
+ })) });
60360
+ var groupCommandsCapability = {
60361
+ name: "group-commands",
60362
+ scope: "device",
60363
+ deviceNative: true,
60364
+ mode: "singleton",
60365
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60366
+ methods: {},
60367
+ status: {
60368
+ schema: GroupCommandsSchema,
60369
+ kind: "push"
60370
+ },
60371
+ runtimeState: GroupCommandsSchema,
60372
+ /**
60373
+ * Runtime-state durability: **session** — the last attempt. A restored
60374
+ * failure would outlive the command that caused it.
60375
+ *
60376
+ * See `RuntimeStateDurability`. Enforced by
60377
+ * `scripts/check-runtime-state-durability.ts`.
60378
+ */
60379
+ durability: "session",
60380
+ history: "none"
60381
+ };
60382
+ /**
60383
+ * How many members of a helper group answered (D944). Counts are integers,
60384
+ * not measurements: zero included is a real count, never a stand-in reading.
60385
+ * The reading itself stays on the output capability. History is none — the
60386
+ * series that later statistics read is the output cap's own history block.
60387
+ */
60388
+ var GroupStatusSchema = object({
60389
+ /** Members whose current value entered the aggregate. */
60390
+ included: number().int().nonnegative().meta(readingMeta("count", {
60391
+ precision: 0,
60392
+ label: "Reporting"
60393
+ })),
60394
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60395
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60396
+ precision: 0,
60397
+ label: "Excluded"
60398
+ })),
60399
+ /** `included` + `excluded`. */
60400
+ total: number().int().nonnegative().meta(readingMeta("count", {
60401
+ precision: 0,
60402
+ label: "Members"
60403
+ }))
60404
+ });
60405
+ var groupStatusCapability = {
60406
+ name: "group-status",
60407
+ scope: "device",
60408
+ deviceNative: true,
60409
+ mode: "singleton",
60410
+ deviceTypes: [
60411
+ DeviceType.Sensor,
60412
+ DeviceType.Switch,
60413
+ DeviceType.Light
60414
+ ],
60415
+ methods: {},
60416
+ status: {
60417
+ schema: GroupStatusSchema,
60418
+ kind: "push"
60419
+ },
60420
+ runtimeState: GroupStatusSchema,
60421
+ /**
60422
+ * Runtime-state durability: **session** — recomputed from members on the
60423
+ * next source event; a restored count would outlive the readings it named.
60424
+ *
60425
+ * See `RuntimeStateDurability`. Enforced by
60426
+ * `scripts/check-runtime-state-durability.ts`.
60427
+ */
60428
+ durability: "session",
60429
+ history: "none"
60430
+ };
59986
60431
  var HeatingValveStatusSchema = object({
59987
60432
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59988
- position: number().min(0).max(100).nullable(),
60433
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59989
60434
  /** Ms epoch when the slice was last updated (push or poll). */
59990
60435
  lastFetchedAt: number()
59991
60436
  });
@@ -60028,9 +60473,9 @@ var HumidifierStatusSchema = object({
60028
60473
  /** Whether the humidifier is currently on. */
60029
60474
  on: boolean(),
60030
60475
  /** Current measured relative humidity (0..100). Null when not reported. */
60031
- currentHumidity: number().min(0).max(100).nullable(),
60476
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60032
60477
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60033
- targetHumidity: number().min(0).max(100).nullable(),
60478
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60034
60479
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60035
60480
  * device has no mode surface. */
60036
60481
  mode: string$2().nullable(),
@@ -60040,9 +60485,9 @@ var HumidifierStatusSchema = object({
60040
60485
  * `idle` / `off`). Null when the device doesn't report it. */
60041
60486
  action: string$2().nullable(),
60042
60487
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60043
- minHumidity: number().nullable(),
60488
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60044
60489
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60045
- maxHumidity: number().nullable(),
60490
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60046
60491
  /** Ms epoch when the slice was last updated. */
60047
60492
  lastChangedAt: number()
60048
60493
  });
@@ -60112,22 +60557,13 @@ var humidifierCapability = {
60112
60557
  * `sourceInfo`.
60113
60558
  */
60114
60559
  var HumiditySensorStatusSchema = object({
60115
- /** Current relative humidity, 0..100. */
60116
- percent: number().min(0).max(100).meta({
60117
- unit: "%",
60560
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60561
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60562
+ precision: 0,
60118
60563
  label: "Humidity"
60119
- }),
60564
+ })),
60120
60565
  /** Ms epoch when the slice was last updated. */
60121
- lastFetchedAt: number(),
60122
- /** Live display unit from the upstream source (e.g. HA
60123
- * `attributes.unit_of_measurement`). The UI prefers this over the
60124
- * role's canonical unit. Absent → fall back to the canonical unit. */
60125
- unit: string$2().optional(),
60126
- /** Suggested decimal places for numeric display.
60127
- * Populated live from the upstream source when provided (e.g. HA
60128
- * `attributes.suggested_display_precision`). Falls back to
60129
- * auto-formatting when absent. */
60130
- precision: number().int().min(0).max(10).optional()
60566
+ lastFetchedAt: number()
60131
60567
  });
60132
60568
  var humiditySensorCapability = {
60133
60569
  name: "humidity-sensor",
@@ -60158,12 +60594,7 @@ var humiditySensorCapability = {
60158
60594
  } } },
60159
60595
  /** Clock fields: written, but excluded from the compare that decides
60160
60596
  * whether persisting is worth a SQLite commit. */
60161
- volatileStateFields: ["lastFetchedAt"],
60162
- customize: {
60163
- version: 1,
60164
- advanced: "derive",
60165
- targets: []
60166
- }
60597
+ volatileStateFields: ["lastFetchedAt"]
60167
60598
  };
60168
60599
  /**
60169
60600
  * Image display cap. Models a single still image exposed by an integration —
@@ -60184,7 +60615,7 @@ var ImageStatusSchema = object({
60184
60615
  * entity exposes no `entity_picture` (yet). */
60185
60616
  url: string$2().nullable(),
60186
60617
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60187
- lastUpdated: number().nullable()
60618
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60188
60619
  });
60189
60620
  var imageCapability = {
60190
60621
  name: "image",
@@ -60272,19 +60703,19 @@ var NormalizedRangeSchema = object({
60272
60703
  */
60273
60704
  var ImageSettingsStatusSchema = object({
60274
60705
  /** Normalized 0–100. */
60275
- brightness: number().optional(),
60706
+ brightness: number().optional().meta(readingMeta("none")),
60276
60707
  /** Normalized 0–100. */
60277
- contrast: number().optional(),
60708
+ contrast: number().optional().meta(readingMeta("none")),
60278
60709
  /** Normalized 0–100. */
60279
- saturation: number().optional(),
60710
+ saturation: number().optional().meta(readingMeta("none")),
60280
60711
  /** Normalized 0–100. */
60281
- sharpness: number().optional(),
60712
+ sharpness: number().optional().meta(readingMeta("none")),
60282
60713
  mirror: boolean().optional(),
60283
60714
  flip: boolean().optional(),
60284
60715
  rotate: ImageRotateSchema.optional(),
60285
60716
  whiteBalance: WhiteBalanceModeSchema.optional(),
60286
60717
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60287
- warmth: number().optional(),
60718
+ warmth: number().optional().meta(readingMeta("none")),
60288
60719
  exposureMode: ExposureModeSchema.optional(),
60289
60720
  backlightMode: BacklightModeSchema.optional(),
60290
60721
  lastFetchedAt: number()
@@ -60563,10 +60994,10 @@ var IntercomAbilitySchema = object({
60563
60994
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60564
60995
  codecs: array(string$2()),
60565
60996
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60566
- sampleRate: number(),
60997
+ sampleRate: number().meta(readingMeta("none")),
60567
60998
  duplex: _enum(["half", "full"]),
60568
60999
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60569
- maxBacklogMs: number()
61000
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60570
61001
  });
60571
61002
  var IntercomStatusSchema = object({
60572
61003
  /** True while a talk session is open. */
@@ -60793,13 +61224,13 @@ var LawnMowerControlStatusSchema = object({
60793
61224
  activity: LawnMowerActivitySchema,
60794
61225
  /** 0..100 battery percentage. Null when the device has no battery
60795
61226
  * reading. */
60796
- batteryLevel: number().min(0).max(100).nullable(),
61227
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60797
61228
  /** 0..100 mowing-completion percentage of the current task, or null when no
60798
61229
  * task is active / progress is unavailable. */
60799
- progressPercent: number().min(0).max(100).nullable(),
61230
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60800
61231
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60801
61232
  * or null when unknown. */
60802
- currentCode: number().nullable(),
61233
+ currentCode: number().nullable().meta(readingMeta("none")),
60803
61234
  /** Human label for {@link currentCode}, or null when undecodable. */
60804
61235
  currentCodeLabel: string$2().nullable(),
60805
61236
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61332,7 +61763,7 @@ var MediaPlayerStatusSchema = object({
61332
61763
  state: MediaPlayerStateSchema,
61333
61764
  /** Volume as 0..100 inclusive. Null when the device has no volume
61334
61765
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61335
- volumeLevel: number().min(0).max(100).nullable(),
61766
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61336
61767
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61337
61768
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61338
61769
  isMuted: boolean().nullable(),
@@ -61345,10 +61776,10 @@ var MediaPlayerStatusSchema = object({
61345
61776
  currentMedia: MediaInfoSchema.nullable(),
61346
61777
  /** Current playback position in ms. Null when not seekable or
61347
61778
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61348
- positionMs: number().int().nonnegative().nullable(),
61779
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61349
61780
  /** Total duration of the current media in ms. Null when unknown
61350
61781
  * (live stream) or nothing is playing. */
61351
- durationMs: number().int().nonnegative().nullable(),
61782
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61352
61783
  /** Shuffle toggle. Null when no shuffle surface.
61353
61784
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61354
61785
  shuffle: boolean().nullable(),
@@ -61657,7 +62088,7 @@ var motionCustomize = {
61657
62088
  eventName: "onMotionChanged"
61658
62089
  },
61659
62090
  deviceTypes: [DeviceType.Camera],
61660
- apply: "replace",
62091
+ apply: "add",
61661
62092
  applyAuthority: {
61662
62093
  kind: "camera-sensors",
61663
62094
  timeoutField: "motionCooldownMs"
@@ -61729,7 +62160,7 @@ var MotionStatusSchema = object({
61729
62160
  * it, every provider seeded it at boot, and every session left it
61730
62161
  * `detected: true` after its detach.
61731
62162
  */
61732
- autoClearAfterMs: number().nullable()
62163
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61733
62164
  });
61734
62165
  /**
61735
62166
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61766,12 +62197,18 @@ var motionCapability = {
61766
62197
  methods: {
61767
62198
  /**
61768
62199
  * Pull the current motion state synchronously. Convenience shortcut
61769
- * for consumers that don't need the full status object; equivalent
61770
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61771
- * `getStatus` once the auto-injected status surface lands in every
61772
- * consumer.
62200
+ * for consumers that don't need the full status object. A composed
62201
+ * group answers from its slice (D944): unknown throws, it is not off.
61773
62202
  */
61774
62203
  isDetected: method(object({ deviceId: number() }), boolean()) },
62204
+ /**
62205
+ * A composed motion group serves `isDetected` from `detected` (D944).
62206
+ * Without this the cap stays uncomposable (`has-commands`).
62207
+ */
62208
+ composedMethods: { isDetected: {
62209
+ kind: "slice",
62210
+ field: "detected"
62211
+ } },
61775
62212
  events: {
61776
62213
  /**
61777
62214
  * Fires every time the runner transitions a camera between
@@ -61911,7 +62348,7 @@ var MotionZoneRegionSchema = object({
61911
62348
  * the grid region(s). */
61912
62349
  var MotionZoneStatusSchema = object({
61913
62350
  enabled: boolean(),
61914
- sensitivity: number(),
62351
+ sensitivity: number().meta(readingMeta("none")),
61915
62352
  /** Grid region(s). Today exactly one `grid` shape. */
61916
62353
  regions: array(MotionZoneRegionSchema),
61917
62354
  lastFetchedAt: number()
@@ -62281,7 +62718,7 @@ var NavigationStatusSchema = object({
62281
62718
  following: boolean(),
62282
62719
  flash: boolean(),
62283
62720
  lightMode: NavigationLightModeSchema,
62284
- lightLevel: number().min(40).max(100),
62721
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62285
62722
  /** Ms epoch when the slice was last updated. */
62286
62723
  lastChangedAt: number()
62287
62724
  });
@@ -62415,13 +62852,13 @@ var NetworkLinkStatusSchema = object({
62415
62852
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62416
62853
  * SKIP a null rather than coerce it.
62417
62854
  */
62418
- signalPercent: number().min(0).max(100).nullable(),
62855
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62419
62856
  /** Raw received signal strength in dBm, when the firmware reports one. */
62420
- rssiDbm: number().optional(),
62857
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62421
62858
  /** Network name of a wireless link, when the firmware reports it. */
62422
62859
  ssid: string$2().optional(),
62423
62860
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62424
- lastUpdated: number()
62861
+ lastUpdated: number().meta(readingMeta("none"))
62425
62862
  });
62426
62863
  var networkLinkCapability = {
62427
62864
  name: "network-link",
@@ -62806,7 +63243,7 @@ var NotifierStatusSchema = object({
62806
63243
  lastError: string$2().nullable(),
62807
63244
  /** Number of deliveries currently buffered server-side (rate-limited
62808
63245
  * notifiers may queue). 0 when the provider sends synchronously. */
62809
- queueDepth: number().int().nonnegative(),
63246
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62810
63247
  /** Per-feature capability matrix — authoritative source for the
62811
63248
  * compose-form field gating. */
62812
63249
  supports: NotifierSupportsSchema
@@ -62898,11 +63335,9 @@ onSent: { data: object({
62898
63335
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62899
63336
  * for: water flow, distance, weight, frequency, signal strength, …).
62900
63337
  *
62901
- * The `unit` and `precision` fields carry the live display metadata that
62902
- * the upstream source provides with each state push (HA
62903
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62904
- * The UI reads them directly from the slice — they are the single source
62905
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63338
+ * The quantity is not on this slice. It is chosen when the device is
63339
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63340
+ * that quantity's base unit. `null` is a refused sample (D393).
62906
63341
  *
62907
63342
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62908
63343
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62911,15 +63346,8 @@ onSent: { data: object({
62911
63346
  * numeric values land here.
62912
63347
  */
62913
63348
  var NumericSensorStatusSchema = object({
62914
- value: number(),
62915
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62916
- * Populated live from the upstream source on each state push.
62917
- * Absent for unitless sensors. */
62918
- unit: string$2().optional(),
62919
- /** Suggested decimal places for numeric display.
62920
- * Populated live from the upstream source when provided.
62921
- * Falls back to auto-formatting when absent. */
62922
- precision: number().int().min(0).max(10).optional(),
63349
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63350
+ value: number().nullable().meta(readingMeta("device")),
62923
63351
  /** Ms epoch when the slice was last updated. */
62924
63352
  lastFetchedAt: number()
62925
63353
  });
@@ -62943,11 +63371,11 @@ var numericSensorCapability = {
62943
63371
  */
62944
63372
  durability: "restored",
62945
63373
  /**
62946
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63374
+ * History (D850): recorded series. The unit is the device quantity's base,
63375
+ * resolved when the series is read — the cap has no single unit.
62947
63376
  */
62948
63377
  history: { fields: { value: {
62949
63378
  kind: "gauge",
62950
- unitFrom: "unit",
62951
63379
  minIntervalMs: 3e4
62952
63380
  } } },
62953
63381
  /** Clock fields: written, but excluded from the compare that decides
@@ -63239,23 +63667,26 @@ var PetFeederStatusSchema = object({
63239
63667
  /** Food currently in the bowl (grams). Null when the device has not
63240
63668
  * reported a reading yet. On dual-hopper models this is the combined
63241
63669
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63242
- foodLevel: number().nullable(),
63670
+ foodLevel: number().nullable().meta(readingMeta("none")),
63243
63671
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63244
63672
  * single-hopper models. */
63245
- food1: number().nullable(),
63673
+ food1: number().nullable().meta(readingMeta("none")),
63246
63674
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63247
63675
  * single-hopper models. */
63248
- food2: number().nullable(),
63676
+ food2: number().nullable().meta(readingMeta("none")),
63249
63677
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63250
63678
  * (`device_class: problem`, on = low). True when the bowl is empty /
63251
63679
  * below the feeder's low threshold. */
63252
63680
  lowFood: boolean(),
63253
63681
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63254
63682
  * device has no battery reading. */
63255
- batteryPower: number().min(0).max(100).nullable(),
63256
- /** Days of desiccant life remaining. Null when the model has no
63257
- * desiccant sensor. */
63258
- desiccantLeftDays: number().nullable(),
63683
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63684
+ /**
63685
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63686
+ * Providers convert from the days the feeder reports. Null when the
63687
+ * model has no desiccant sensor.
63688
+ */
63689
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63259
63690
  /** True while a feed is in progress. */
63260
63691
  feeding: boolean(),
63261
63692
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63266,7 +63697,7 @@ var PetFeederStatusSchema = object({
63266
63697
  * with `errorCode` for consumers that want the raw integer. */
63267
63698
  error: string$2().nullable(),
63268
63699
  /** Raw device error code (0 / null = no error). */
63269
- errorCode: number().nullable(),
63700
+ errorCode: number().nullable().meta(readingMeta("none")),
63270
63701
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63271
63702
  isDualHopper: boolean(),
63272
63703
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63276,7 +63707,7 @@ var PetFeederStatusSchema = object({
63276
63707
  /** Play a chime when dispensing. */
63277
63708
  feedSound: boolean(),
63278
63709
  /** Speaker / prompt volume level (device-scaled integer). */
63279
- volume: number(),
63710
+ volume: number().meta(readingMeta("none")),
63280
63711
  /** Ms epoch when the slice was last refreshed from the cloud. */
63281
63712
  lastFetchedAt: number()
63282
63713
  });
@@ -63395,7 +63826,7 @@ var petFeederCapability = {
63395
63826
  },
63396
63827
  desiccantLeftDays: {
63397
63828
  kind: "gauge",
63398
- unit: "days"
63829
+ unit: "s"
63399
63830
  }
63400
63831
  } }
63401
63832
  };
@@ -63796,36 +64227,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63796
64227
  */
63797
64228
  var PowerMeterStatusSchema = object({
63798
64229
  /** Instantaneous power draw in watts. */
63799
- watts: number().optional().meta({
63800
- unit: "W",
63801
- label: "Power"
63802
- }),
64230
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63803
64231
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63804
- kwhTotal: number().optional().meta({
63805
- unit: "kWh",
63806
- label: "Energy"
63807
- }),
64232
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63808
64233
  /** Voltage in volts. */
63809
- volts: number().optional(),
64234
+ volts: number().optional().meta(readingMeta("voltage")),
63810
64235
  /** Current in amperes. */
63811
- amps: number().optional(),
63812
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63813
- va: number().optional(),
64236
+ amps: number().optional().meta(readingMeta("current")),
64237
+ /** Apparent power in volt-amperes. */
64238
+ va: number().optional().meta(readingMeta("apparent-power")),
63814
64239
  /** Power factor (0..1) if reported. */
63815
- powerFactor: number().min(0).max(1).optional(),
64240
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63816
64241
  /** Ms epoch when the slice was last updated. */
63817
- lastFetchedAt: number(),
63818
- /** Live display unit of the single metric this slice carries (e.g. HA
63819
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63820
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63821
- * unit per slice is unambiguous. The UI prefers this over the role's
63822
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63823
- unit: string$2().optional(),
63824
- /** Suggested decimal places for numeric display.
63825
- * Populated live from the upstream source when provided (e.g. HA
63826
- * `attributes.suggested_display_precision`). Falls back to
63827
- * auto-formatting when absent. */
63828
- precision: number().int().min(0).max(10).optional()
64242
+ lastFetchedAt: number()
63829
64243
  });
63830
64244
  var powerMeterCapability = {
63831
64245
  name: "power-meter",
@@ -63889,12 +64303,7 @@ var powerMeterCapability = {
63889
64303
  } },
63890
64304
  /** Clock fields: written, but excluded from the compare that decides
63891
64305
  * whether persisting is worth a SQLite commit. */
63892
- volatileStateFields: ["lastFetchedAt"],
63893
- customize: {
63894
- version: 1,
63895
- advanced: "derive",
63896
- targets: []
63897
- }
64306
+ volatileStateFields: ["lastFetchedAt"]
63898
64307
  };
63899
64308
  /**
63900
64309
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63914,11 +64323,11 @@ var powerMeterCapability = {
63914
64323
  */
63915
64324
  var GpsLocationSchema = object({
63916
64325
  /** Latitude in decimal degrees, -90..90. */
63917
- latitude: number().min(-90).max(90),
64326
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63918
64327
  /** Longitude in decimal degrees, -180..180. */
63919
- longitude: number().min(-180).max(180),
64328
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63920
64329
  /** Reported accuracy in meters (lower = better). */
63921
- accuracyMeters: number().nonnegative()
64330
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63922
64331
  });
63923
64332
  var PresenceStatusSchema = object({
63924
64333
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63930,7 +64339,7 @@ var PresenceStatusSchema = object({
63930
64339
  gps: GpsLocationSchema.nullable(),
63931
64340
  /** Optional battery level of the tracking device (0..100). Null
63932
64341
  * when not reported. */
63933
- batteryPercent: number().min(0).max(100).nullable(),
64342
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63934
64343
  /** Ms epoch when the slice was last updated. */
63935
64344
  lastChangedAt: number()
63936
64345
  });
@@ -63980,19 +64389,10 @@ var presenceCapability = {
63980
64389
  * `sourceInfo`.
63981
64390
  */
63982
64391
  var PressureSensorStatusSchema = object({
63983
- /** Current pressure in hPa. */
63984
- hpa: number(),
64392
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64393
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63985
64394
  /** Ms epoch when the slice was last updated. */
63986
- lastFetchedAt: number(),
63987
- /** Live display unit from the upstream source (e.g. HA
63988
- * `attributes.unit_of_measurement`). The UI prefers this over the
63989
- * role's canonical unit. Absent → fall back to the canonical unit. */
63990
- unit: string$2().optional(),
63991
- /** Suggested decimal places for numeric display.
63992
- * Populated live from the upstream source when provided (e.g. HA
63993
- * `attributes.suggested_display_precision`). Falls back to
63994
- * auto-formatting when absent. */
63995
- precision: number().int().min(0).max(10).optional()
64395
+ lastFetchedAt: number()
63996
64396
  });
63997
64397
  var pressureSensorCapability = {
63998
64398
  name: "pressure-sensor",
@@ -64406,8 +64806,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64406
64806
  }), _void(), { kind: "mutation" });
64407
64807
  var PtzAutotrackSettingsSchema = object({
64408
64808
  targetType: string$2().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64409
- stopDelaySeconds: number().int().min(0).max(300),
64410
- disappearDelaySeconds: number().int().min(0).max(300)
64809
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64810
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64411
64811
  });
64412
64812
  /**
64413
64813
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64561,7 +64961,7 @@ var RunningStateStatusSchema = object({
64561
64961
  /** null = unknown — never coerced to false (D393). */
64562
64962
  running: boolean().nullable(),
64563
64963
  /** Epoch ms the current boolean began; null while `running` is null. */
64564
- since: number().nullable(),
64964
+ since: number().nullable().meta(readingMeta("none")),
64565
64965
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64566
64966
  phase: string$2().nullable(),
64567
64967
  /** null until a reading has been published (D393). */
@@ -64576,7 +64976,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64576
64976
  /** Generic Customize target for the running boolean (D936). */
64577
64977
  var runningCustomize = {
64578
64978
  version: 1,
64579
- advanced: "derive",
64580
64979
  targets: [{
64581
64980
  id: "running",
64582
64981
  cardinality: "scalar",
@@ -64596,7 +64995,7 @@ var runningCustomize = {
64596
64995
  DeviceType.Thermostat,
64597
64996
  DeviceType.PetFeeder
64598
64997
  ],
64599
- apply: "add-or-replace",
64998
+ apply: "add",
64600
64999
  path: "running",
64601
65000
  sources: [
64602
65001
  {
@@ -65860,9 +66259,9 @@ var RecordTriggerSchema = _enum([
65860
66259
  */
65861
66260
  var RecordWindowSchema = object({
65862
66261
  trigger: RecordTriggerSchema,
65863
- day: number().int().min(0).max(6),
65864
- startMinute: number().int().min(0).max(1439),
65865
- endMinute: number().int().min(1).max(1440)
66262
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66263
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66264
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65866
66265
  });
65867
66266
  /** Status of one physical volume, as the camera itself describes it. */
65868
66267
  var OnboardStorageVolumeSchema = object({
@@ -65938,10 +66337,10 @@ var RecordingOnboardStatusSchema = object({
65938
66337
  /** Master "record to the card at all" switch. */
65939
66338
  enabled: boolean().nullable(),
65940
66339
  overwriteWhenFull: boolean().nullable(),
65941
- preRecordSec: number().nullable(),
65942
- postRecordSec: number().nullable(),
66340
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66341
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65943
66342
  /** Length of one recorded file, in minutes. */
65944
- segmentMinutes: number().nullable(),
66343
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65945
66344
  /** The primary track's weekly windows, flattened. */
65946
66345
  windows: array(RecordWindowSchema),
65947
66346
  /**
@@ -65957,7 +66356,7 @@ var RecordingOnboardStatusSchema = object({
65957
66356
  * provider reporting a non-zero count also reports the schedule as not
65958
66357
  * writable.
65959
66358
  */
65960
- unreadableWindows: number(),
66359
+ unreadableWindows: number().meta(readingMeta("count")),
65961
66360
  /**
65962
66361
  * The camera is scheduled to record and has NO usable storage.
65963
66362
  *
@@ -66802,7 +67201,7 @@ var CamStreamDescriptorSchema = object({
66802
67201
  url: string$2().optional(),
66803
67202
  codec: string$2().optional(),
66804
67203
  resolution: CamStreamResolutionSchema.optional(),
66805
- fps: number().positive().optional(),
67204
+ fps: number().positive().optional().meta(readingMeta("rate")),
66806
67205
  label: string$2().optional(),
66807
67206
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66808
67207
  deviceFeatures: array(string$2()).optional(),
@@ -66889,18 +67288,18 @@ var StreamProfileSchema = _enum([
66889
67288
  ]);
66890
67289
  /** The current encoding config of one stream profile. */
66891
67290
  var StreamProfileConfigSchema = object({
66892
- width: number(),
66893
- height: number(),
67291
+ width: number().meta(readingMeta("none")),
67292
+ height: number().meta(readingMeta("none")),
66894
67293
  codec: _enum(["h264", "h265"]),
66895
- framerate: number(),
66896
- bitrate: number(),
67294
+ framerate: number().meta(readingMeta("rate")),
67295
+ bitrate: number().meta(readingMeta("bitrate")),
66897
67296
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66898
67297
  encoderProfile: _enum([
66899
67298
  "high",
66900
67299
  "main",
66901
67300
  "baseline"
66902
67301
  ]).optional(),
66903
- gop: number().optional(),
67302
+ gop: number().optional().meta(readingMeta("none")),
66904
67303
  /**
66905
67304
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66906
67305
  *
@@ -67121,12 +67520,7 @@ var switchCapability = {
67121
67520
  label: "On",
67122
67521
  kind: "boolean"
67123
67522
  }
67124
- }] },
67125
- customize: {
67126
- version: 1,
67127
- advanced: "derive",
67128
- targets: []
67129
- }
67523
+ }] }
67130
67524
  };
67131
67525
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67132
67526
  function hasControlCharacter(value) {
@@ -67709,35 +68103,21 @@ var tamperCapability = {
67709
68103
  * entries with `device_class: temperature` and any future native
67710
68104
  * thermometer.
67711
68105
  *
67712
- * Unit normalisation: providers convert to Celsius before storing.
67713
- * The slice value is always Celsius so cross-cap aggregators
67714
- * (climate-control's `currentTemp`, energy analytics) can compose
67715
- * without per-source unit fixups. The canonical display unit (`°C`) is
67716
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67717
- * `sourceInfo`.
68106
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68107
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67718
68108
  *
67719
68109
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67720
68110
  * frozen feed (provider hung) distinct from a "temperature hasn't
67721
68111
  * changed" steady state.
67722
68112
  */
67723
68113
  var TemperatureSensorStatusSchema = object({
67724
- /** Current temperature in Celsius. */
67725
- celsius: number().meta({
67726
- unit: "°C",
68114
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68115
+ celsius: number().nullable().meta(readingMeta("temperature", {
68116
+ precision: 1,
67727
68117
  label: "Temperature"
67728
- }),
68118
+ })),
67729
68119
  /** Ms epoch when the slice was last updated (push or poll). */
67730
- lastFetchedAt: number(),
67731
- /** Live display unit from the upstream source (e.g. HA
67732
- * `attributes.unit_of_measurement`). The UI prefers this over the
67733
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67734
- * Absent → the UI falls back to the role's canonical unit. */
67735
- unit: string$2().optional(),
67736
- /** Suggested decimal places for numeric display.
67737
- * Populated live from the upstream source when provided (e.g. HA
67738
- * `attributes.suggested_display_precision`). Falls back to
67739
- * auto-formatting when absent. */
67740
- precision: number().int().min(0).max(10).optional()
68120
+ lastFetchedAt: number()
67741
68121
  });
67742
68122
  var temperatureSensorCapability = {
67743
68123
  name: "temperature-sensor",
@@ -67768,12 +68148,7 @@ var temperatureSensorCapability = {
67768
68148
  } } },
67769
68149
  /** Clock fields: written, but excluded from the compare that decides
67770
68150
  * whether persisting is worth a SQLite commit. */
67771
- volatileStateFields: ["lastFetchedAt"],
67772
- customize: {
67773
- version: 1,
67774
- advanced: "derive",
67775
- targets: []
67776
- }
68151
+ volatileStateFields: ["lastFetchedAt"]
67777
68152
  };
67778
68153
  /**
67779
68154
  * toast — system-scoped singleton capability that streams toast
@@ -68056,16 +68431,16 @@ var VacuumControlStatusSchema = object({
68056
68431
  ]),
68057
68432
  /** 0..100 battery percentage. Null when the device has no battery
68058
68433
  * reading. */
68059
- batteryLevel: number().min(0).max(100).nullable(),
68434
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68060
68435
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68061
68436
  * the vacuum has no speed control. */
68062
68437
  fanSpeed: string$2().nullable(),
68063
68438
  /** Speed tokens the hardware accepts — drives the UI selector. */
68064
68439
  availableFanSpeeds: array(string$2()),
68065
68440
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68066
- progressPercent: number().min(0).max(100).nullable(),
68441
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68067
68442
  /** Current error code (0 / null = no error). */
68068
- errorCode: number().nullable(),
68443
+ errorCode: number().nullable().meta(readingMeta("none")),
68069
68444
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68070
68445
  errorLabel: string$2().nullable(),
68071
68446
  /** Ms epoch when the slice was last updated. */
@@ -68143,7 +68518,7 @@ var ValveStatusSchema = object({
68143
68518
  ]),
68144
68519
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68145
68520
  * intermediate position surface. */
68146
- position: number().min(0).max(100).nullable(),
68521
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68147
68522
  /** Ms epoch when the slice was last updated. */
68148
68523
  lastChangedAt: number()
68149
68524
  });
@@ -68251,9 +68626,9 @@ var vibrationCapability = {
68251
68626
  */
68252
68627
  var WaterHeaterStatusSchema = object({
68253
68628
  /** Current measured temperature. Null when not reported. */
68254
- currentTemp: number().nullable(),
68629
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68255
68630
  /** Target temperature setpoint. Null when no setpoint surface. */
68256
- targetTemp: number().nullable(),
68631
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68257
68632
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68258
68633
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68259
68634
  * device reports an unknown state. */
@@ -68264,9 +68639,9 @@ var WaterHeaterStatusSchema = object({
68264
68639
  * has no away surface. */
68265
68640
  away: boolean().nullable(),
68266
68641
  /** HA `min_temp` attribute. Null when not reported. */
68267
- minTemp: number().nullable(),
68642
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68268
68643
  /** HA `max_temp` attribute. Null when not reported. */
68269
- maxTemp: number().nullable(),
68644
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68270
68645
  /** Ms epoch when the slice was last updated. */
68271
68646
  lastChangedAt: number()
68272
68647
  });
@@ -68351,25 +68726,18 @@ var waterHeaterCapability = {
68351
68726
  * Forecast deferred — current conditions only for v1.
68352
68727
  */
68353
68728
  var WeatherStatusSchema = object({
68354
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68355
- * when no condition has been reported yet. */
68729
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68356
68730
  condition: string$2().nullable(),
68357
- /** Current temperature in the reported unit. Null when not provided. */
68358
- temperature: number().nullable(),
68359
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68360
- temperatureUnit: string$2().nullable(),
68731
+ /** Current temperature in Celsius. Null when not provided or refused. */
68732
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68361
68733
  /** Relative humidity (0..100). Null when not provided. */
68362
- humidity: number().min(0).max(100).nullable(),
68363
- /** Barometric pressure in the reported unit. Null when not provided. */
68364
- pressure: number().nullable(),
68365
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68366
- pressureUnit: string$2().nullable(),
68367
- /** Wind speed in the reported unit. Null when not provided. */
68368
- windSpeed: number().nullable(),
68369
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68370
- windSpeedUnit: string$2().nullable(),
68734
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68735
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68736
+ pressure: number().nullable().meta(readingMeta("pressure")),
68737
+ /** Wind speed in metres per second. Null when not provided or refused. */
68738
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68371
68739
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68372
- windBearing: number().nullable(),
68740
+ windBearing: number().nullable().meta(readingMeta("angle")),
68373
68741
  /** Ms epoch when the slice was last updated. */
68374
68742
  lastFetchedAt: number()
68375
68743
  });
@@ -68397,12 +68765,12 @@ var weatherCapability = {
68397
68765
  */
68398
68766
  durability: "session",
68399
68767
  /**
68400
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68768
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68401
68769
  */
68402
68770
  history: { fields: {
68403
68771
  temperature: {
68404
68772
  kind: "gauge",
68405
- unitFrom: "temperatureUnit",
68773
+ unit: "°C",
68406
68774
  deadband: .2
68407
68775
  },
68408
68776
  humidity: {
@@ -68412,12 +68780,12 @@ var weatherCapability = {
68412
68780
  },
68413
68781
  pressure: {
68414
68782
  kind: "gauge",
68415
- unitFrom: "pressureUnit",
68783
+ unit: "hPa",
68416
68784
  deadband: .5
68417
68785
  },
68418
68786
  windSpeed: {
68419
68787
  kind: "gauge",
68420
- unitFrom: "windSpeedUnit",
68788
+ unit: "m/s",
68421
68789
  deadband: .5
68422
68790
  }
68423
68791
  } }
@@ -69648,6 +70016,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69648
70016
  featureProbe: featureProbeCapability,
69649
70017
  flood: floodCapability,
69650
70018
  gas: gasCapability,
70019
+ groupCommands: groupCommandsCapability,
70020
+ groupStatus: groupStatusCapability,
69651
70021
  heatingValve: heatingValveCapability,
69652
70022
  humidifier: humidifierCapability,
69653
70023
  humiditySensor: humiditySensorCapability,
@@ -71024,6 +71394,13 @@ Object.freeze({
71024
71394
  access: "view",
71025
71395
  role: "admin"
71026
71396
  },
71397
+ "addons.getPackageDocs": {
71398
+ capName: "addons",
71399
+ capScope: "system",
71400
+ addonId: null,
71401
+ access: "view",
71402
+ role: "admin"
71403
+ },
71027
71404
  "addons.getVersions": {
71028
71405
  capName: "addons",
71029
71406
  capScope: "system",
@@ -74132,6 +74509,20 @@ Object.freeze({
74132
74509
  access: "view",
74133
74510
  role: "viewer"
74134
74511
  },
74512
+ "groupCommands.getStatus": {
74513
+ capName: "group-commands",
74514
+ capScope: "device",
74515
+ addonId: null,
74516
+ access: "view",
74517
+ role: "viewer"
74518
+ },
74519
+ "groupStatus.getStatus": {
74520
+ capName: "group-status",
74521
+ capScope: "device",
74522
+ addonId: null,
74523
+ access: "view",
74524
+ role: "viewer"
74525
+ },
74135
74526
  "heatingValve.getStatus": {
74136
74527
  capName: "heating-valve",
74137
74528
  capScope: "device",
@@ -83954,6 +84345,37 @@ var SENSOR_CAP_VOCABULARY = {
83954
84345
  icon: "flame"
83955
84346
  }]
83956
84347
  },
84348
+ "group-commands": {
84349
+ scope: "device",
84350
+ noun: t("Group command", "Comando di gruppo")
84351
+ },
84352
+ "group-status": {
84353
+ scope: "device",
84354
+ noun: t("Group", "Gruppo"),
84355
+ state: [
84356
+ {
84357
+ kind: "numeric",
84358
+ field: "included",
84359
+ quantity: t("the members reporting", "i membri che rispondono"),
84360
+ label: t("Reporting", "Rispondono"),
84361
+ icon: "users"
84362
+ },
84363
+ {
84364
+ kind: "numeric",
84365
+ field: "excluded",
84366
+ quantity: t("the members left out", "i membri esclusi"),
84367
+ label: t("Excluded", "Esclusi"),
84368
+ icon: "users"
84369
+ },
84370
+ {
84371
+ kind: "numeric",
84372
+ field: "total",
84373
+ quantity: t("the member count", "il numero di membri"),
84374
+ label: t("Members", "Membri"),
84375
+ icon: "sigma"
84376
+ }
84377
+ ]
84378
+ },
83957
84379
  "humidity-sensor": {
83958
84380
  scope: "device",
83959
84381
  noun: t("Humidity", "Umidità"),
@@ -83987,7 +84409,6 @@ var SENSOR_CAP_VOCABULARY = {
83987
84409
  field: "value",
83988
84410
  quantity: t("the reading", "il valore"),
83989
84411
  label: t("Reading", "Valore"),
83990
- unitField: "unit",
83991
84412
  icon: "gauge"
83992
84413
  }]
83993
84414
  },
@@ -84123,7 +84544,7 @@ var SENSOR_CAP_VOCABULARY = {
84123
84544
  field: "temperature",
84124
84545
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84125
84546
  label: t("Temperature", "Temperatura"),
84126
- unitField: "temperatureUnit",
84547
+ unit: "°C",
84127
84548
  icon: "thermometer"
84128
84549
  },
84129
84550
  {
@@ -84139,7 +84560,7 @@ var SENSOR_CAP_VOCABULARY = {
84139
84560
  field: "windSpeed",
84140
84561
  quantity: t("the wind speed", "la velocità del vento"),
84141
84562
  label: t("Wind", "Vento"),
84142
- unitField: "windSpeedUnit",
84563
+ unit: "m/s",
84143
84564
  icon: "wind"
84144
84565
  },
84145
84566
  {
@@ -84147,7 +84568,7 @@ var SENSOR_CAP_VOCABULARY = {
84147
84568
  field: "pressure",
84148
84569
  quantity: t("the air pressure", "la pressione atmosferica"),
84149
84570
  label: t("Pressure", "Pressione"),
84150
- unitField: "pressureUnit",
84571
+ unit: "hPa",
84151
84572
  icon: "gauge"
84152
84573
  }
84153
84574
  ]
@@ -85420,6 +85841,16 @@ Object.freeze({
85420
85841
  form: "single",
85421
85842
  optional: false
85422
85843
  }],
85844
+ "groupCommands.getStatus": [{
85845
+ name: "deviceId",
85846
+ form: "single",
85847
+ optional: false
85848
+ }],
85849
+ "groupStatus.getStatus": [{
85850
+ name: "deviceId",
85851
+ form: "single",
85852
+ optional: false
85853
+ }],
85423
85854
  "heatingValve.getStatus": [{
85424
85855
  name: "deviceId",
85425
85856
  form: "single",
@@ -87404,6 +87835,8 @@ new Map(Object.entries({
87404
87835
  "detection-blind": "device-events",
87405
87836
  "motion-noisy": "device-events",
87406
87837
  "motion-calm": "device-events",
87838
+ "package-llm-silent": "device-events",
87839
+ "package-llm-answered": "device-events",
87407
87840
  "recording-stalled": "device-events",
87408
87841
  "recording-healthy": "device-events",
87409
87842
  "recording-degraded": "device-events",
@@ -87487,6 +87920,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87487
87920
  label: "Motion without a subject",
87488
87921
  kinds: ["motion-noisy", "motion-calm"]
87489
87922
  },
87923
+ {
87924
+ id: "package-llm",
87925
+ family: "device-events",
87926
+ label: "Package confirm",
87927
+ kinds: ["package-llm-silent", "package-llm-answered"]
87928
+ },
87490
87929
  {
87491
87930
  id: "recording",
87492
87931
  family: "device-events",
@@ -87668,6 +88107,8 @@ var DEVICE_EVENTS = [
87668
88107
  "detection-blind",
87669
88108
  "motion-noisy",
87670
88109
  "motion-calm",
88110
+ "package-llm-silent",
88111
+ "package-llm-answered",
87671
88112
  "recording-stalled",
87672
88113
  "recording-healthy",
87673
88114
  "recording-degraded",
@@ -87936,6 +88377,15 @@ new Map([
87936
88377
  snapshotLabel: "Cameras with constant motion now",
87937
88378
  snapshotSection: null
87938
88379
  },
88380
+ {
88381
+ id: "package-llm-confirm",
88382
+ family: "device-events",
88383
+ label: "Package confirm (not answering / answering again)",
88384
+ bad: ["package-llm-silent"],
88385
+ recovery: ["package-llm-answered"],
88386
+ snapshotLabel: "Cameras whose package confirm is not answering",
88387
+ snapshotSection: null
88388
+ },
87939
88389
  {
87940
88390
  id: "recording-health",
87941
88391
  family: "device-events",
@@ -197439,7 +197889,6 @@ function decodeElectricalPower(attributeId, value) {
197439
197889
  cap: "power-meter",
197440
197890
  patch: {
197441
197891
  watts: mw / 1e3,
197442
- unit: "W",
197443
197892
  lastFetchedAt: Date.now()
197444
197893
  }
197445
197894
  };