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