@camstack/addon-provider-homematic 1.2.157 → 1.2.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -8,7 +8,7 @@ let http = require("http");
8
8
  let crypto$1 = require("crypto");
9
9
  let fs_promises = require("fs/promises");
10
10
  let path = require("path");
11
- //#region ../types/dist/event-category-DHXxB8t9.mjs
11
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
12
12
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
13
13
  EventCategory["SystemBoot"] = "system.boot";
14
14
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -334,6 +334,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
334
334
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
335
335
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
336
336
  /**
337
+ * A camera's package LLM confirm has failed closed three times in a row
338
+ * (D931, 2026-10-26). One edge per camera until
339
+ * {@link PipelinePackageLlmAnswered}.
340
+ */
341
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
342
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
343
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
344
+ /**
337
345
  * Per-camera pipeline config was mutated by the orchestrator
338
346
  * (a settings change via `setAgentInferenceDevices` /
339
347
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5576,7 +5584,7 @@ var ZodIssueCode = {
5576
5584
  var ZodFirstPartyTypeKind;
5577
5585
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5578
5586
  //#endregion
5579
- //#region ../types/dist/sleep-BErAxhul.mjs
5587
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5580
5588
  /**
5581
5589
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5582
5590
  * window to float samples (D455).
@@ -8318,6 +8326,99 @@ function defineEntryKind(name, spec) {
8318
8326
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8319
8327
  return defined;
8320
8328
  }
8329
+ var READING_QUANTITIES = [
8330
+ "temperature",
8331
+ "pressure",
8332
+ "speed",
8333
+ "precipitation",
8334
+ "precipitation-rate",
8335
+ "length",
8336
+ "area",
8337
+ "illuminance",
8338
+ "irradiance",
8339
+ "power",
8340
+ "energy",
8341
+ "voltage",
8342
+ "current",
8343
+ "apparent-power",
8344
+ "humidity",
8345
+ "percentage",
8346
+ "concentration-volume",
8347
+ "concentration-mass",
8348
+ "concentration-ppb",
8349
+ "volume",
8350
+ "duration",
8351
+ "duration-ms",
8352
+ "information",
8353
+ "sound-level",
8354
+ "signal-level",
8355
+ "colour-temperature",
8356
+ "angle",
8357
+ "bitrate",
8358
+ "rate",
8359
+ "mass",
8360
+ "index",
8361
+ "ratio",
8362
+ "count",
8363
+ "none",
8364
+ "device"
8365
+ ];
8366
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8367
+ new Set(READING_QUANTITIES);
8368
+ /**
8369
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8370
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8371
+ */
8372
+ var QUANTITY_BASE = {
8373
+ temperature: "°C",
8374
+ pressure: "hPa",
8375
+ speed: "m/s",
8376
+ precipitation: "mm",
8377
+ "precipitation-rate": "mm/h",
8378
+ length: "m",
8379
+ area: "m²",
8380
+ illuminance: "lx",
8381
+ irradiance: "W/m²",
8382
+ power: "W",
8383
+ energy: "kWh",
8384
+ voltage: "V",
8385
+ current: "A",
8386
+ "apparent-power": "VA",
8387
+ humidity: "%",
8388
+ percentage: "%",
8389
+ "concentration-volume": "ppm",
8390
+ "concentration-mass": "µg/m³",
8391
+ "concentration-ppb": "ppb",
8392
+ volume: "L",
8393
+ duration: "s",
8394
+ "duration-ms": "ms",
8395
+ information: "B",
8396
+ "sound-level": "dBFS",
8397
+ "signal-level": "dBm",
8398
+ "colour-temperature": "mired",
8399
+ angle: "°",
8400
+ bitrate: "kbps",
8401
+ rate: "fps",
8402
+ mass: "kg",
8403
+ index: "none",
8404
+ ratio: "none",
8405
+ count: "none",
8406
+ none: "none",
8407
+ device: "device"
8408
+ };
8409
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8410
+ function baseUnitOf(quantity) {
8411
+ const symbol = QUANTITY_BASE[quantity];
8412
+ if (symbol === "none" || symbol === "device") return void 0;
8413
+ return symbol;
8414
+ }
8415
+ function readingMeta(quantity, extras) {
8416
+ return {
8417
+ quantity,
8418
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8419
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8420
+ };
8421
+ }
8321
8422
  /** Shared Zod schemas used across streaming capabilities. */
8322
8423
  var CamProfileSchema = _enum([
8323
8424
  "high",
@@ -8345,8 +8446,8 @@ var CamStreamKindSchema = _enum([
8345
8446
  "derived"
8346
8447
  ]);
8347
8448
  var CamStreamResolutionSchema = object({
8348
- width: number().int().positive(),
8349
- height: number().int().positive()
8449
+ width: number().int().positive().meta(readingMeta("none")),
8450
+ height: number().int().positive().meta(readingMeta("none"))
8350
8451
  });
8351
8452
  var CameraStreamSchema = object({
8352
8453
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8864,6 +8965,8 @@ var CAP_NAMES = Object.freeze([
8864
8965
  "filesystem-browse",
8865
8966
  "flood",
8866
8967
  "gas",
8968
+ "group-commands",
8969
+ "group-status",
8867
8970
  "heating-valve",
8868
8971
  "humidifier",
8869
8972
  "humidity-sensor",
@@ -8993,14 +9096,6 @@ Object.freeze({
8993
9096
  {
8994
9097
  "path": "aqi",
8995
9098
  "valueType": "number"
8996
- },
8997
- {
8998
- "path": "unit",
8999
- "valueType": "string"
9000
- },
9001
- {
9002
- "path": "precision",
9003
- "valueType": "number"
9004
9099
  }
9005
9100
  ],
9006
9101
  "alarm-panel": [
@@ -9036,20 +9131,10 @@ Object.freeze({
9036
9131
  "valueType": "boolean"
9037
9132
  }
9038
9133
  ],
9039
- "ambient-light-sensor": [
9040
- {
9041
- "path": "lux",
9042
- "valueType": "number"
9043
- },
9044
- {
9045
- "path": "unit",
9046
- "valueType": "string"
9047
- },
9048
- {
9049
- "path": "precision",
9050
- "valueType": "number"
9051
- }
9052
- ],
9134
+ "ambient-light-sensor": [{
9135
+ "path": "lux",
9136
+ "valueType": "number"
9137
+ }],
9053
9138
  "audio-metrics": [
9054
9139
  {
9055
9140
  "path": "ts",
@@ -9129,6 +9214,10 @@ Object.freeze({
9129
9214
  "path": "lastUpdated",
9130
9215
  "valueType": "number"
9131
9216
  },
9217
+ {
9218
+ "path": "volts",
9219
+ "valueType": "number"
9220
+ },
9132
9221
  {
9133
9222
  "path": "lastContactAt",
9134
9223
  "valueType": "number"
@@ -9371,10 +9460,6 @@ Object.freeze({
9371
9460
  "datetime"
9372
9461
  ]
9373
9462
  },
9374
- {
9375
- "path": "unit",
9376
- "valueType": "string"
9377
- },
9378
9463
  {
9379
9464
  "path": "min",
9380
9465
  "valueType": "number"
@@ -9386,19 +9471,6 @@ Object.freeze({
9386
9471
  {
9387
9472
  "path": "step",
9388
9473
  "valueType": "number"
9389
- },
9390
- {
9391
- "path": "precision",
9392
- "valueType": "number"
9393
- },
9394
- {
9395
- "path": "format",
9396
- "valueType": "enum",
9397
- "enumValues": [
9398
- "date",
9399
- "time",
9400
- "datetime"
9401
- ]
9402
9474
  }
9403
9475
  ],
9404
9476
  "cover": [
@@ -9639,13 +9711,8 @@ Object.freeze({
9639
9711
  "path": "value",
9640
9712
  "valueType": "string"
9641
9713
  }, {
9642
- "path": "format",
9643
- "valueType": "enum",
9644
- "enumValues": [
9645
- "date",
9646
- "time",
9647
- "datetime"
9648
- ]
9714
+ "path": "timestamp",
9715
+ "valueType": "boolean"
9649
9716
  }],
9650
9717
  "event-emitter": [
9651
9718
  {
@@ -9718,6 +9785,20 @@ Object.freeze({
9718
9785
  "path": "detected",
9719
9786
  "valueType": "boolean"
9720
9787
  }],
9788
+ "group-status": [
9789
+ {
9790
+ "path": "included",
9791
+ "valueType": "number"
9792
+ },
9793
+ {
9794
+ "path": "excluded",
9795
+ "valueType": "number"
9796
+ },
9797
+ {
9798
+ "path": "total",
9799
+ "valueType": "number"
9800
+ }
9801
+ ],
9721
9802
  "heating-valve": [{
9722
9803
  "path": "position",
9723
9804
  "valueType": "number"
@@ -9752,20 +9833,10 @@ Object.freeze({
9752
9833
  "valueType": "number"
9753
9834
  }
9754
9835
  ],
9755
- "humidity-sensor": [
9756
- {
9757
- "path": "percent",
9758
- "valueType": "number"
9759
- },
9760
- {
9761
- "path": "unit",
9762
- "valueType": "string"
9763
- },
9764
- {
9765
- "path": "precision",
9766
- "valueType": "number"
9767
- }
9768
- ],
9836
+ "humidity-sensor": [{
9837
+ "path": "percent",
9838
+ "valueType": "number"
9839
+ }],
9769
9840
  "image": [{
9770
9841
  "path": "url",
9771
9842
  "valueType": "string"
@@ -10104,20 +10175,10 @@ Object.freeze({
10104
10175
  "valueType": "boolean"
10105
10176
  }
10106
10177
  ],
10107
- "numeric-sensor": [
10108
- {
10109
- "path": "value",
10110
- "valueType": "number"
10111
- },
10112
- {
10113
- "path": "unit",
10114
- "valueType": "string"
10115
- },
10116
- {
10117
- "path": "precision",
10118
- "valueType": "number"
10119
- }
10120
- ],
10178
+ "numeric-sensor": [{
10179
+ "path": "value",
10180
+ "valueType": "number"
10181
+ }],
10121
10182
  "pet-feeder": [
10122
10183
  {
10123
10184
  "path": "foodLevel",
@@ -10209,14 +10270,6 @@ Object.freeze({
10209
10270
  {
10210
10271
  "path": "powerFactor",
10211
10272
  "valueType": "number"
10212
- },
10213
- {
10214
- "path": "unit",
10215
- "valueType": "string"
10216
- },
10217
- {
10218
- "path": "precision",
10219
- "valueType": "number"
10220
10273
  }
10221
10274
  ],
10222
10275
  "presence": [
@@ -10245,20 +10298,10 @@ Object.freeze({
10245
10298
  "valueType": "number"
10246
10299
  }
10247
10300
  ],
10248
- "pressure-sensor": [
10249
- {
10250
- "path": "hpa",
10251
- "valueType": "number"
10252
- },
10253
- {
10254
- "path": "unit",
10255
- "valueType": "string"
10256
- },
10257
- {
10258
- "path": "precision",
10259
- "valueType": "number"
10260
- }
10261
- ],
10301
+ "pressure-sensor": [{
10302
+ "path": "hpa",
10303
+ "valueType": "number"
10304
+ }],
10262
10305
  "privacy-mask": [{
10263
10306
  "path": "enabled",
10264
10307
  "valueType": "boolean"
@@ -10569,20 +10612,10 @@ Object.freeze({
10569
10612
  "path": "tampered",
10570
10613
  "valueType": "boolean"
10571
10614
  }],
10572
- "temperature-sensor": [
10573
- {
10574
- "path": "celsius",
10575
- "valueType": "number"
10576
- },
10577
- {
10578
- "path": "unit",
10579
- "valueType": "string"
10580
- },
10581
- {
10582
- "path": "precision",
10583
- "valueType": "number"
10584
- }
10585
- ],
10615
+ "temperature-sensor": [{
10616
+ "path": "celsius",
10617
+ "valueType": "number"
10618
+ }],
10586
10619
  "update": [
10587
10620
  {
10588
10621
  "path": "currentVersion",
@@ -10693,10 +10726,6 @@ Object.freeze({
10693
10726
  "path": "temperature",
10694
10727
  "valueType": "number"
10695
10728
  },
10696
- {
10697
- "path": "temperatureUnit",
10698
- "valueType": "string"
10699
- },
10700
10729
  {
10701
10730
  "path": "humidity",
10702
10731
  "valueType": "number"
@@ -10705,18 +10734,10 @@ Object.freeze({
10705
10734
  "path": "pressure",
10706
10735
  "valueType": "number"
10707
10736
  },
10708
- {
10709
- "path": "pressureUnit",
10710
- "valueType": "string"
10711
- },
10712
10737
  {
10713
10738
  "path": "windSpeed",
10714
10739
  "valueType": "number"
10715
10740
  },
10716
- {
10717
- "path": "windSpeedUnit",
10718
- "valueType": "string"
10719
- },
10720
10741
  {
10721
10742
  "path": "windBearing",
10722
10743
  "valueType": "number"
@@ -11119,7 +11140,7 @@ function errMsg(err) {
11119
11140
  return String(err);
11120
11141
  }
11121
11142
  //#endregion
11122
- //#region ../types/dist/composition-kEl9mevu.mjs
11143
+ //#region ../types/dist/composition-OhpuespA.mjs
11123
11144
  /**
11124
11145
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11125
11146
  *
@@ -11874,15 +11895,74 @@ function validateExpressionSource(src) {
11874
11895
  }
11875
11896
  return null;
11876
11897
  }
11877
- /** A composed device's stableId is this prefix plus the block id. */
11878
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11879
- /** Longest snippet a `code` source may carry (slice 4). */
11880
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11898
+ /**
11899
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11900
+ * id. Split out so helper schemas can name it without a cycle through
11901
+ * `composition.ts` (D659).
11902
+ */
11881
11903
  var CompositionSourceRefSchema = object({
11882
11904
  /** The owning addon; `stableId` is unique only within it. */
11883
11905
  addonId: string().min(1),
11884
11906
  stableId: string().min(1)
11885
11907
  });
11908
+ var GROUP_TEMPLATE_IDS = [
11909
+ "temperature",
11910
+ "humidity",
11911
+ "pressure",
11912
+ "illuminance",
11913
+ "power",
11914
+ "energy",
11915
+ "battery",
11916
+ "numeric",
11917
+ "motion",
11918
+ "contact",
11919
+ "leak",
11920
+ "smoke",
11921
+ "occupancy",
11922
+ "binary",
11923
+ "switch",
11924
+ "light"
11925
+ ];
11926
+ var NUMERIC_AGGREGATES = [
11927
+ "mean",
11928
+ "median",
11929
+ "min",
11930
+ "max",
11931
+ "sum"
11932
+ ];
11933
+ var BINARY_AGGREGATES = ["any", "all"];
11934
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11935
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11936
+ var AGGREGATE_REDUCERS = [
11937
+ "mean",
11938
+ "median",
11939
+ "min",
11940
+ "max",
11941
+ "sum",
11942
+ "any",
11943
+ "all"
11944
+ ];
11945
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11946
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11947
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11948
+ /** One member. The field is stored so occupancy can name motion or binary. */
11949
+ var HelperMemberSchema = object({
11950
+ source: CompositionSourceRefSchema,
11951
+ cap: string().min(1),
11952
+ fieldPath: string().min(1)
11953
+ });
11954
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11955
+ kind: literal("group"),
11956
+ templateId: GroupTemplateIdSchema,
11957
+ aggregate: AggregateReducerSchema,
11958
+ members: array(HelperMemberSchema).min(1).max(64),
11959
+ /** Actuator groups only. Readings leave this unset. */
11960
+ active: ActiveCriterionSchema.optional()
11961
+ })]);
11962
+ /** A composed device's stableId is this prefix plus the block id. */
11963
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11964
+ /** Longest snippet a `code` source may carry (slice 4). */
11965
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11886
11966
  /** One field of one capability of one source device. */
11887
11967
  var CompositionFieldReadSchema = object({
11888
11968
  source: CompositionSourceRefSchema,
@@ -11926,7 +12006,33 @@ var CompositionCodeSourceSchema = object({
11926
12006
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11927
12007
  CompositionFromSourceSchema,
11928
12008
  CompositionExpressionSourceSchema,
11929
- CompositionCodeSourceSchema
12009
+ CompositionCodeSourceSchema,
12010
+ object({
12011
+ kind: literal("aggregate"),
12012
+ reducer: _enum([
12013
+ "mean",
12014
+ "median",
12015
+ "min",
12016
+ "max",
12017
+ "sum",
12018
+ "any",
12019
+ "all"
12020
+ ]),
12021
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12022
+ /** `value` is the reading; the others are the member counts. */
12023
+ publish: _enum([
12024
+ "value",
12025
+ "included",
12026
+ "excluded",
12027
+ "total"
12028
+ ]),
12029
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12030
+ empty: _enum([
12031
+ "null",
12032
+ "omit",
12033
+ "offline"
12034
+ ])
12035
+ })
11930
12036
  ]);
11931
12037
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11932
12038
  kind: literal("forward"),
@@ -11972,7 +12078,13 @@ var CompositionSchema = object({
11972
12078
  kind: literal("existing"),
11973
12079
  device: CompositionSourceRefSchema
11974
12080
  })]),
11975
- features: array(CompositionFeatureSchema).min(1).max(16)
12081
+ features: array(CompositionFeatureSchema).min(1).max(16),
12082
+ /**
12083
+ * Present when the block is a helper template. Absent on a free
12084
+ * composition (the room example, device 4603). The editor reads this,
12085
+ * never the compiled features.
12086
+ */
12087
+ helper: HelperSpecSchema.optional()
11976
12088
  }).superRefine((composition, ctx) => {
11977
12089
  const seen = /* @__PURE__ */ new Set();
11978
12090
  composition.features.forEach((feature, index) => {
@@ -18229,7 +18341,11 @@ var CompositionProblemSchema = object({
18229
18341
  "item-field-reserved",
18230
18342
  "target-missing",
18231
18343
  "target-unreadable",
18232
- "not-replaceable"
18344
+ "not-replaceable",
18345
+ "native-capability-replace",
18346
+ "mixed-quantity",
18347
+ "helper-mismatch",
18348
+ "member-constraint"
18233
18349
  ]),
18234
18350
  path: string(),
18235
18351
  message: string()
@@ -18250,7 +18366,8 @@ var CompositionUnavailableModeSchema = _enum([
18250
18366
  "null",
18251
18367
  "offline",
18252
18368
  "unknown",
18253
- "release"
18369
+ "release",
18370
+ "omit"
18254
18371
  ]);
18255
18372
  /**
18256
18373
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18287,9 +18404,11 @@ var CompositionFieldPlanSchema = object({
18287
18404
  reads: array(CompositionFieldReadSchema)
18288
18405
  });
18289
18406
  /**
18290
- * What a feature does to its target (D663): `new` builds a capability on a new
18291
- * device; on an existing device it `add`s a capability the device lacks, or
18292
- * `replace`s named fields of one it has natively.
18407
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18408
+ * new device; on an existing device it `add`s a capability the device lacks.
18409
+ * Replacing a capability the device already serves is refused
18410
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18411
+ * wire for a claim the hub already holds.
18293
18412
  */
18294
18413
  var CompositionFeatureModeSchema = _enum([
18295
18414
  "new",
@@ -18462,6 +18581,275 @@ Object.freeze({
18462
18581
  result: ["number", "null"]
18463
18582
  }
18464
18583
  });
18584
+ /**
18585
+ * One catalog row per base reading a group helper can build (D944 §3).
18586
+ * Data, not a form default written into the block.
18587
+ */
18588
+ var NOW = {
18589
+ fieldPath: "lastFetchedAt",
18590
+ expr: "now"
18591
+ };
18592
+ var CHANGED = {
18593
+ fieldPath: "lastChangedAt",
18594
+ expr: "now"
18595
+ };
18596
+ function numeric(input) {
18597
+ return {
18598
+ ...input,
18599
+ family: "numeric",
18600
+ deviceType: DeviceType.Sensor,
18601
+ memberDeviceType: DeviceType.Sensor,
18602
+ commandCaps: [],
18603
+ baseUnit: baseUnitOf(input.quantity),
18604
+ aggregates: NUMERIC_AGGREGATES,
18605
+ accept: [{
18606
+ cap: input.outputCap,
18607
+ fieldPath: input.outputField
18608
+ }]
18609
+ };
18610
+ }
18611
+ function binary(input) {
18612
+ return {
18613
+ ...input,
18614
+ family: "binary",
18615
+ deviceType: DeviceType.Sensor,
18616
+ memberDeviceType: DeviceType.Sensor,
18617
+ commandCaps: [],
18618
+ quantity: "none",
18619
+ baseUnit: void 0,
18620
+ aggregates: BINARY_AGGREGATES,
18621
+ empty: "offline"
18622
+ };
18623
+ }
18624
+ var GROUP_TEMPLATES = [
18625
+ numeric({
18626
+ id: "temperature",
18627
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18628
+ role: DeviceRole.TemperatureSensor,
18629
+ outputCap: "temperature-sensor",
18630
+ outputField: "celsius",
18631
+ quantity: "temperature",
18632
+ defaultAggregate: "mean",
18633
+ empty: "null",
18634
+ companions: [NOW]
18635
+ }),
18636
+ numeric({
18637
+ id: "humidity",
18638
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18639
+ role: DeviceRole.HumiditySensor,
18640
+ outputCap: "humidity-sensor",
18641
+ outputField: "percent",
18642
+ quantity: "humidity",
18643
+ defaultAggregate: "mean",
18644
+ empty: "null",
18645
+ companions: [NOW]
18646
+ }),
18647
+ numeric({
18648
+ id: "pressure",
18649
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18650
+ role: DeviceRole.PressureSensor,
18651
+ outputCap: "pressure-sensor",
18652
+ outputField: "hpa",
18653
+ quantity: "pressure",
18654
+ defaultAggregate: "mean",
18655
+ empty: "null",
18656
+ companions: [NOW]
18657
+ }),
18658
+ numeric({
18659
+ id: "illuminance",
18660
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18661
+ role: DeviceRole.AmbientLightSensor,
18662
+ outputCap: "ambient-light-sensor",
18663
+ outputField: "lux",
18664
+ quantity: "illuminance",
18665
+ defaultAggregate: "mean",
18666
+ empty: "null",
18667
+ companions: [NOW]
18668
+ }),
18669
+ numeric({
18670
+ id: "power",
18671
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18672
+ role: DeviceRole.PowerSensor,
18673
+ outputCap: "power-meter",
18674
+ outputField: "watts",
18675
+ quantity: "power",
18676
+ defaultAggregate: "sum",
18677
+ empty: "omit",
18678
+ companions: [NOW]
18679
+ }),
18680
+ numeric({
18681
+ id: "energy",
18682
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18683
+ role: DeviceRole.EnergySensor,
18684
+ outputCap: "power-meter",
18685
+ outputField: "kwhTotal",
18686
+ quantity: "energy",
18687
+ defaultAggregate: "sum",
18688
+ empty: "omit",
18689
+ companions: [NOW]
18690
+ }),
18691
+ numeric({
18692
+ id: "battery",
18693
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18694
+ role: DeviceRole.BatterySensor,
18695
+ outputCap: "battery",
18696
+ outputField: "percentage",
18697
+ quantity: "percentage",
18698
+ defaultAggregate: "mean",
18699
+ empty: "null",
18700
+ companions: [
18701
+ {
18702
+ fieldPath: "charging",
18703
+ expr: "\"unknown\""
18704
+ },
18705
+ {
18706
+ fieldPath: "sleeping",
18707
+ expr: "false"
18708
+ },
18709
+ {
18710
+ fieldPath: "lastUpdated",
18711
+ expr: "now"
18712
+ }
18713
+ ]
18714
+ }),
18715
+ numeric({
18716
+ id: "numeric",
18717
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18718
+ role: DeviceRole.NumericSensor,
18719
+ outputCap: "numeric-sensor",
18720
+ outputField: "value",
18721
+ quantity: "device",
18722
+ defaultAggregate: "mean",
18723
+ empty: "null",
18724
+ companions: [NOW]
18725
+ }),
18726
+ binary({
18727
+ id: "motion",
18728
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18729
+ role: DeviceRole.MotionSensor,
18730
+ outputCap: "motion",
18731
+ outputField: "detected",
18732
+ defaultAggregate: "any",
18733
+ accept: [{
18734
+ cap: "motion",
18735
+ fieldPath: "detected"
18736
+ }],
18737
+ companions: [{
18738
+ fieldPath: "lastDetectedAt",
18739
+ expr: "null"
18740
+ }, {
18741
+ fieldPath: "autoClearAfterMs",
18742
+ expr: "null"
18743
+ }]
18744
+ }),
18745
+ binary({
18746
+ id: "contact",
18747
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18748
+ role: DeviceRole.ContactSensor,
18749
+ outputCap: "contact",
18750
+ outputField: "entryOpen",
18751
+ defaultAggregate: "any",
18752
+ accept: [{
18753
+ cap: "contact",
18754
+ fieldPath: "entryOpen"
18755
+ }],
18756
+ companions: [CHANGED]
18757
+ }),
18758
+ binary({
18759
+ id: "leak",
18760
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18761
+ role: DeviceRole.LeakSensor,
18762
+ outputCap: "flood",
18763
+ outputField: "flooded",
18764
+ defaultAggregate: "any",
18765
+ accept: [{
18766
+ cap: "flood",
18767
+ fieldPath: "flooded"
18768
+ }],
18769
+ companions: [CHANGED]
18770
+ }),
18771
+ binary({
18772
+ id: "smoke",
18773
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18774
+ role: DeviceRole.SmokeSensor,
18775
+ outputCap: "smoke",
18776
+ outputField: "detected",
18777
+ defaultAggregate: "any",
18778
+ accept: [{
18779
+ cap: "smoke",
18780
+ fieldPath: "detected"
18781
+ }],
18782
+ companions: [CHANGED]
18783
+ }),
18784
+ binary({
18785
+ id: "occupancy",
18786
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18787
+ role: DeviceRole.BinarySensor,
18788
+ outputCap: "binary",
18789
+ outputField: "on",
18790
+ defaultAggregate: "any",
18791
+ accept: [{
18792
+ cap: "motion",
18793
+ fieldPath: "detected"
18794
+ }, {
18795
+ cap: "binary",
18796
+ fieldPath: "on"
18797
+ }],
18798
+ companions: [CHANGED]
18799
+ }),
18800
+ binary({
18801
+ id: "binary",
18802
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18803
+ role: DeviceRole.BinarySensor,
18804
+ outputCap: "binary",
18805
+ outputField: "on",
18806
+ defaultAggregate: "any",
18807
+ accept: [{
18808
+ cap: "binary",
18809
+ fieldPath: "on"
18810
+ }],
18811
+ companions: [CHANGED]
18812
+ }),
18813
+ actuator({
18814
+ id: "switch",
18815
+ 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.",
18816
+ deviceType: DeviceType.Switch,
18817
+ outputCap: "switch",
18818
+ commandCaps: ["switch"]
18819
+ }),
18820
+ actuator({
18821
+ id: "light",
18822
+ 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.",
18823
+ deviceType: DeviceType.Light,
18824
+ outputCap: "switch",
18825
+ commandCaps: [
18826
+ "switch",
18827
+ "brightness",
18828
+ "color"
18829
+ ]
18830
+ })
18831
+ ];
18832
+ function actuator(input) {
18833
+ return {
18834
+ ...input,
18835
+ family: "actuator",
18836
+ role: void 0,
18837
+ memberDeviceType: input.deviceType,
18838
+ outputField: "on",
18839
+ quantity: "none",
18840
+ baseUnit: void 0,
18841
+ aggregates: BINARY_AGGREGATES,
18842
+ defaultAggregate: "any",
18843
+ empty: "offline",
18844
+ accept: [{
18845
+ cap: "switch",
18846
+ fieldPath: "on"
18847
+ }],
18848
+ companions: [CHANGED]
18849
+ };
18850
+ }
18851
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18852
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18465
18853
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18466
18854
  /**
18467
18855
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20288,12 +20676,12 @@ function buildDeviceAdminUrl(parts) {
20288
20676
  * providers detect renames by matching `uniqueId` and patch `id`
20289
20677
  * in-place so dispatch keeps working.
20290
20678
  *
20291
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20292
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20293
- * state push and are the single source of truth. The `device_class` tag is
20294
- * an HA-specific input that is consumed at the discovery boundary
20295
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20296
- * stored after that point.
20679
+ * **Display unit and precision are NOT stored here.** They are a read-time
20680
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20681
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20682
+ * neither is repeated on each sample. The `device_class` tag is an
20683
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20684
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20297
20685
  *
20298
20686
  * The `raw` escape hatch carries provider-specific attributes that don't
20299
20687
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20307,6 +20695,14 @@ var SourceInfoSchema = object({
20307
20695
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20308
20696
  * Used to detect rename when `id` changes. */
20309
20697
  uniqueId: string().optional(),
20698
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20699
+ quantity: ReadingQuantitySchema.optional(),
20700
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20701
+ dateTimeShape: _enum([
20702
+ "date",
20703
+ "time",
20704
+ "datetime"
20705
+ ]).optional(),
20310
20706
  /** Provider-specific extras that don't fit the structured slots.
20311
20707
  * Whitelist what goes here per provider — do NOT dump entire HA
20312
20708
  * attribute blobs (would explode the meta JSON column). */
@@ -20352,6 +20748,80 @@ function mergeSourceInfo(prev, patch) {
20352
20748
  return next;
20353
20749
  }
20354
20750
  /**
20751
+ * Coerce common unit-of-measurement string variants to a canonical form
20752
+ * so the UI's display templates can rely on stable strings. Pure
20753
+ * passthrough for unknown units — kept conservative on purpose, the goal
20754
+ * is to normalise the long tail of HA `unit_of_measurement` values that
20755
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
20756
+ *
20757
+ * Extend the map as we encounter real-world data. Returning the raw
20758
+ * input for unknown values is intentional: providers should never
20759
+ * lose information silently here.
20760
+ */
20761
+ function normalizeUnit(raw) {
20762
+ if (raw == null) return void 0;
20763
+ const trimmed = raw.trim();
20764
+ if (trimmed === "") return void 0;
20765
+ const lower = trimmed.toLowerCase();
20766
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
20767
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
20768
+ if (lower === "k" || lower === "kelvin") return "K";
20769
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
20770
+ if (lower === "lx" || lower === "lux") return "lx";
20771
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
20772
+ if (lower === "kpa") return "kPa";
20773
+ if (lower === "pa") return "Pa";
20774
+ if (lower === "mbar" || lower === "millibar") return "mbar";
20775
+ if (lower === "w" || lower === "watt") return "W";
20776
+ if (lower === "kw" || lower === "kilowatt") return "kW";
20777
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
20778
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
20779
+ if (lower === "v" || lower === "volt") return "V";
20780
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
20781
+ if (lower === "ma" || lower === "milliampere") return "mA";
20782
+ if (lower === "va") return "VA";
20783
+ if (lower === "ppm") return "ppm";
20784
+ if (lower === "ppb") return "ppb";
20785
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
20786
+ if (lower === "km/h" || lower === "kph") return "km/h";
20787
+ if (lower === "mph") return "mph";
20788
+ if (lower === "m/s") return "m/s";
20789
+ if (lower === "m") return "m";
20790
+ if (lower === "m²" || lower === "m2") return "m²";
20791
+ if (lower === "mm") return "mm";
20792
+ if (lower === "cm") return "cm";
20793
+ if (lower === "km") return "km";
20794
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
20795
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
20796
+ if (lower === "kg") return "kg";
20797
+ if (lower === "g") return "g";
20798
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
20799
+ if (lower === "ml") return "mL";
20800
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
20801
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
20802
+ if (lower === "ms") return "ms";
20803
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
20804
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
20805
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
20806
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
20807
+ if (lower === "kib") return "KiB";
20808
+ if (lower === "mib") return "MiB";
20809
+ if (lower === "gib") return "GiB";
20810
+ if (lower === "tib") return "TiB";
20811
+ if (lower === "kb") return "kB";
20812
+ if (lower === "mb") return "MB";
20813
+ if (lower === "gb") return "GB";
20814
+ if (lower === "dbm") return "dBm";
20815
+ if (lower === "dbfs") return "dBFS";
20816
+ if (lower === "inhg") return "inHg";
20817
+ if (lower === "mm/h") return "mm/h";
20818
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
20819
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
20820
+ if (lower === "kbps") return "kbps";
20821
+ if (lower === "fps") return "fps";
20822
+ return trimmed;
20823
+ }
20824
+ /**
20355
20825
  * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
20356
20826
  * blob is a free-form `Record<string, unknown>` shared with hardware-
20357
20827
  * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
@@ -41428,7 +41898,8 @@ method(object({
41428
41898
  "enum"
41429
41899
  ]),
41430
41900
  enumValues: array(string()).optional(),
41431
- item: boolean().optional()
41901
+ item: boolean().optional(),
41902
+ quantity: ReadingQuantitySchema.optional()
41432
41903
  })).readonly(),
41433
41904
  itemArray: object({
41434
41905
  path: string(),
@@ -45272,8 +45743,8 @@ var MaskPolygonShapeSchema = object({
45272
45743
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45273
45744
  var MaskGridShapeSchema = object({
45274
45745
  kind: literal("grid"),
45275
- gridWidth: number(),
45276
- gridHeight: number(),
45746
+ gridWidth: number().meta(readingMeta("none")),
45747
+ gridHeight: number().meta(readingMeta("none")),
45277
45748
  cells: array(boolean())
45278
45749
  });
45279
45750
  discriminatedUnion("kind", [
@@ -45385,6 +45856,8 @@ var NcSystemEventKindSchema = _enum([
45385
45856
  "detection-blind",
45386
45857
  "motion-noisy",
45387
45858
  "motion-calm",
45859
+ "package-llm-silent",
45860
+ "package-llm-answered",
45388
45861
  "recording-stalled",
45389
45862
  "recording-healthy",
45390
45863
  "recording-degraded",
@@ -56476,6 +56949,28 @@ var PackageVersionInfoSchema = object({
56476
56949
  deprecated: string().optional(),
56477
56950
  distTags: array(string()).readonly()
56478
56951
  });
56952
+ /**
56953
+ * One published document. `ok` is markdown from the registry. `missing` means
56954
+ * the registry answered and the file was not there. `error` is a registry or
56955
+ * tarball failure — never an empty string standing in for either.
56956
+ */
56957
+ var PackageDocBodySchema = discriminatedUnion("status", [
56958
+ object({
56959
+ status: literal("ok"),
56960
+ markdown: string()
56961
+ }),
56962
+ object({ status: literal("missing") }),
56963
+ object({
56964
+ status: literal("error"),
56965
+ message: string()
56966
+ })
56967
+ ]);
56968
+ var PackageDocsSchema = object({
56969
+ name: string(),
56970
+ version: string(),
56971
+ readme: PackageDocBodySchema,
56972
+ changelog: PackageDocBodySchema
56973
+ });
56479
56974
  var SearchResultSchema = object({
56480
56975
  name: string(),
56481
56976
  version: string(),
@@ -56694,7 +57189,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56694
57189
  }), object({ success: literal(true) }), {
56695
57190
  kind: "mutation",
56696
57191
  auth: "admin"
56697
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57192
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57193
+ name: string().min(1),
57194
+ version: string().min(1).optional()
57195
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56698
57196
  kind: "mutation",
56699
57197
  auth: "admin"
56700
57198
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56753,27 +57251,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56753
57251
  */
56754
57252
  var AirQualitySensorStatusSchema = object({
56755
57253
  /** Carbon dioxide concentration in ppm. */
56756
- co2Ppm: number().min(0).optional(),
57254
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56757
57255
  /** Total volatile organic compounds in ppb. */
56758
- vocPpb: number().min(0).optional(),
57256
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56759
57257
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56760
- pm25: number().min(0).optional(),
57258
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56761
57259
  /** Particulate matter ≤ 10 μm in µg/m³. */
56762
- pm10: number().min(0).optional(),
57260
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56763
57261
  /** Composite AQI value (typically 0..500). */
56764
- aqi: number().optional(),
57262
+ aqi: number().optional().meta(readingMeta("index")),
56765
57263
  /** Ms epoch when the slice was last updated. */
56766
- lastFetchedAt: number(),
56767
- /** Live display unit of the single metric this slice carries (e.g. HA
56768
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56769
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56770
- * per slice is unambiguous. */
56771
- unit: string().optional(),
56772
- /** Suggested decimal places for numeric display.
56773
- * Populated live from the upstream source when provided (e.g. HA
56774
- * `attributes.suggested_display_precision`). Falls back to
56775
- * auto-formatting when absent. */
56776
- precision: number().int().min(0).max(10).optional()
57264
+ lastFetchedAt: number()
56777
57265
  });
56778
57266
  var airQualitySensorCapability = {
56779
57267
  name: "air-quality-sensor",
@@ -56810,12 +57298,12 @@ var airQualitySensorCapability = {
56810
57298
  },
56811
57299
  pm25: {
56812
57300
  kind: "gauge",
56813
- unit: "ug/m3",
57301
+ unit: "µg/m³",
56814
57302
  deadband: 1
56815
57303
  },
56816
57304
  pm10: {
56817
57305
  kind: "gauge",
56818
- unit: "ug/m3",
57306
+ unit: "µg/m³",
56819
57307
  deadband: 1
56820
57308
  },
56821
57309
  aqi: {
@@ -56833,22 +57321,13 @@ var airQualitySensorCapability = {
56833
57321
  * entries with `device_class: illuminance`.
56834
57322
  */
56835
57323
  var AmbientLightSensorStatusSchema = object({
56836
- /** Current illuminance in lux (lx). */
56837
- lux: number().min(0).meta({
56838
- unit: "lx",
57324
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57325
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57326
+ precision: 0,
56839
57327
  label: "Illuminance"
56840
- }),
57328
+ })),
56841
57329
  /** Ms epoch when the slice was last updated. */
56842
- lastFetchedAt: number(),
56843
- /** Live display unit from the upstream source (e.g. HA
56844
- * `attributes.unit_of_measurement`). The UI prefers this over the
56845
- * role's canonical unit. Absent → fall back to the canonical unit. */
56846
- unit: string().optional(),
56847
- /** Suggested decimal places for numeric display.
56848
- * Populated live from the upstream source when provided (e.g. HA
56849
- * `attributes.suggested_display_precision`). Falls back to
56850
- * auto-formatting when absent. */
56851
- precision: number().int().min(0).max(10).optional()
57330
+ lastFetchedAt: number()
56852
57331
  });
56853
57332
  var ambientLightSensorCapability = {
56854
57333
  name: "ambient-light-sensor",
@@ -56880,12 +57359,7 @@ var ambientLightSensorCapability = {
56880
57359
  } } },
56881
57360
  /** Clock fields: written, but excluded from the compare that decides
56882
57361
  * whether persisting is worth a SQLite commit. */
56883
- volatileStateFields: ["lastFetchedAt"],
56884
- customize: {
56885
- version: 1,
56886
- advanced: "derive",
56887
- targets: []
56888
- }
57362
+ volatileStateFields: ["lastFetchedAt"]
56889
57363
  };
56890
57364
  /**
56891
57365
  * Per-class audio metrics aggregated over a sliding window.
@@ -57112,10 +57586,7 @@ var BatteryStatusSchema = object({
57112
57586
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57113
57587
  * declines to band a non-finite reading, which is the correct shape.
57114
57588
  */
57115
- percentage: number().min(0).max(100).nullable().meta({
57116
- unit: "%",
57117
- label: "Charge"
57118
- }),
57589
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57119
57590
  /**
57120
57591
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57121
57592
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57142,7 +57613,14 @@ var BatteryStatusSchema = object({
57142
57613
  */
57143
57614
  sleeping: boolean().meta({ label: "Sleeping" }),
57144
57615
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57145
- lastUpdated: number(),
57616
+ lastUpdated: number().meta(readingMeta("none")),
57617
+ /**
57618
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57619
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57620
+ * that only report charge. A second reading on this cap, not a second
57621
+ * `numeric-sensor` on the same device.
57622
+ */
57623
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57146
57624
  /**
57147
57625
  * Ms epoch of the last time the device PROVED it was reachable — a
57148
57626
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57194,7 +57672,6 @@ var BATTERY_DEVICE_TYPES = [
57194
57672
  /** Generic Customize target for the percentage reading (D936). */
57195
57673
  var batteryCustomize = {
57196
57674
  version: 1,
57197
- advanced: "derive",
57198
57675
  targets: [{
57199
57676
  id: "percentage",
57200
57677
  cardinality: "scalar",
@@ -57203,7 +57680,7 @@ var batteryCustomize = {
57203
57680
  stateField: "percentage"
57204
57681
  },
57205
57682
  deviceTypes: BATTERY_DEVICE_TYPES,
57206
- apply: "add-or-replace",
57683
+ apply: "add",
57207
57684
  path: "percentage",
57208
57685
  sources: [{
57209
57686
  kind: "field",
@@ -57324,10 +57801,16 @@ onStatusChanged: { data: object({
57324
57801
  /**
57325
57802
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57326
57803
  */
57327
- history: { fields: { percentage: {
57328
- kind: "gauge",
57329
- unit: "%"
57330
- } } },
57804
+ history: { fields: {
57805
+ percentage: {
57806
+ kind: "gauge",
57807
+ unit: "%"
57808
+ },
57809
+ volts: {
57810
+ kind: "gauge",
57811
+ unit: "V"
57812
+ }
57813
+ } },
57331
57814
  customize: batteryCustomize,
57332
57815
  /** Clock fields: written, but excluded from the compare that decides
57333
57816
  * whether persisting is worth a SQLite commit. */
@@ -57374,12 +57857,7 @@ var binaryCapability = {
57374
57857
  history: { fields: { on: { kind: "boolean" } } },
57375
57858
  /** Clock fields: written, but excluded from the compare that decides
57376
57859
  * whether persisting is worth a SQLite commit. */
57377
- volatileStateFields: ["lastChangedAt"],
57378
- customize: {
57379
- version: 1,
57380
- advanced: "derive",
57381
- targets: []
57382
- }
57860
+ volatileStateFields: ["lastChangedAt"]
57383
57861
  };
57384
57862
  /**
57385
57863
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57395,7 +57873,7 @@ var binaryCapability = {
57395
57873
  */
57396
57874
  var BrightnessStatusSchema = object({
57397
57875
  /** Current level as 0..100 inclusive. Firmware-reported. */
57398
- percentage: number().min(0).max(100),
57876
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57399
57877
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57400
57878
  lastChangedAt: number()
57401
57879
  });
@@ -57849,21 +58327,21 @@ var ClimateControlStatusSchema = object({
57849
58327
  /** Single setpoint in Celsius. Used by single-target modes
57850
58328
  * (heat / cool / dry). Null when the device is in a range mode
57851
58329
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57852
- target: number().nullable(),
58330
+ target: number().nullable().meta(readingMeta("temperature")),
57853
58331
  /** Upper bound of the dual setpoint in Celsius. Populated only
57854
58332
  * in `heat_cool` mode. */
57855
- targetHigh: number().nullable(),
58333
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57856
58334
  /** Lower bound of the dual setpoint in Celsius. Populated only
57857
58335
  * in `heat_cool` mode. */
57858
- targetLow: number().nullable(),
58336
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57859
58337
  /** Target relative humidity (0..100). Null when the device has no
57860
58338
  * humidity control. */
57861
- targetHumidity: number().min(0).max(100).nullable(),
58339
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57862
58340
  /** Current measured temperature in Celsius. Read-only — pushed by
57863
58341
  * the device's internal sensor. */
57864
- currentTemp: number().nullable(),
58342
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57865
58343
  /** Current measured relative humidity (0..100). Read-only. */
57866
- currentHumidity: number().min(0).max(100).nullable(),
58344
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57867
58345
  /** Vertical louver swing on/off. Null when the device has no vertical
57868
58346
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57869
58347
  swingVertical: boolean().nullable(),
@@ -57871,11 +58349,11 @@ var ClimateControlStatusSchema = object({
57871
58349
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57872
58350
  swingHorizontal: boolean().nullable(),
57873
58351
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57874
- minTemp: number().nullable().optional(),
58352
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57875
58353
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57876
- maxTemp: number().nullable().optional(),
58354
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57877
58355
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57878
- tempStep: number().positive().nullable().optional(),
58356
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57879
58357
  /** Ms epoch when the slice was last updated (push or command). */
57880
58358
  lastFetchedAt: number()
57881
58359
  });
@@ -58017,17 +58495,17 @@ var climateControlCapability = {
58017
58495
  * modes to expose in the picker without a slice fetch.
58018
58496
  */
58019
58497
  var RgbTripletSchema = object({
58020
- r: number().int().min(0).max(255),
58021
- g: number().int().min(0).max(255),
58022
- b: number().int().min(0).max(255)
58498
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58499
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58500
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58023
58501
  });
58024
58502
  var HsvTripletSchema = object({
58025
58503
  /** Hue in degrees, 0..360. */
58026
- h: number().min(0).max(360),
58504
+ h: number().min(0).max(360).meta(readingMeta("none")),
58027
58505
  /** Saturation as 0..100 inclusive. */
58028
- s: number().min(0).max(100),
58506
+ s: number().min(0).max(100).meta(readingMeta("none")),
58029
58507
  /** Value/brightness as 0..100 inclusive. */
58030
- v: number().min(0).max(100)
58508
+ v: number().min(0).max(100).meta(readingMeta("none"))
58031
58509
  });
58032
58510
  var ColorInputSchema = discriminatedUnion("mode", [
58033
58511
  object({
@@ -58056,7 +58534,7 @@ var ColorStatusSchema = object({
58056
58534
  /** Populated when `mode === 'hsv'`. */
58057
58535
  hsv: HsvTripletSchema.optional(),
58058
58536
  /** Populated when `mode === 'mired'`. */
58059
- mireds: number().int().optional(),
58537
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58060
58538
  /** Ms epoch of the last operator-driven change. */
58061
58539
  lastChangedAt: number()
58062
58540
  });
@@ -58251,18 +58729,12 @@ _enum([
58251
58729
  "empty",
58252
58730
  "full"
58253
58731
  ]);
58254
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58255
- * each. */
58256
- var ConsumableRemainingUnitSchema = _enum([
58257
- "days",
58258
- "hours",
58259
- "cycles"
58260
- ]);
58261
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58262
- * days). `value` null = unknown (D393). */
58732
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58733
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58734
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58263
58735
  var ConsumableRemainingSchema = object({
58264
- value: number().min(0).nullable(),
58265
- unit: ConsumableRemainingUnitSchema
58736
+ value: number().min(0).nullable().meta(readingMeta("device")),
58737
+ kind: ConsumableRemainingKindSchema
58266
58738
  });
58267
58739
  /** A single consumable item. Either a continuous `level` (remaining
58268
58740
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58275,7 +58747,7 @@ var ConsumableItemSchema = object({
58275
58747
  /** Display name. */
58276
58748
  label: string().min(1),
58277
58749
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58278
- level: number().min(0).max(100).nullable(),
58750
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58279
58751
  /** The provider's discrete verdict when known (binary mode). */
58280
58752
  status: ConsumableStatusSchema.nullable(),
58281
58753
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58357,8 +58829,8 @@ var consumablesCustomize = {
58357
58829
  transforms: [{ kind: "identity" }]
58358
58830
  }],
58359
58831
  literals: [{
58360
- path: "remaining.unit",
58361
- values: [...ConsumableRemainingUnitSchema.options]
58832
+ path: "remaining.kind",
58833
+ values: [...ConsumableRemainingKindSchema.options]
58362
58834
  }]
58363
58835
  },
58364
58836
  {
@@ -58487,7 +58959,6 @@ reset: method(object({
58487
58959
  },
58488
58960
  "remaining.value": {
58489
58961
  kind: "gauge",
58490
- unitFrom: "remaining.unit",
58491
58962
  deadband: 1,
58492
58963
  displayName: "Remaining"
58493
58964
  }
@@ -58557,10 +59028,9 @@ var contactCapability = {
58557
59028
  * (empty array for the other kinds). It lives in the slice because
58558
59029
  * HA `select.*` entities may update their options at runtime.
58559
59030
  *
58560
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58561
- * `kind === 'numeric'` and are absent (undefined) for all other
58562
- * kinds. They are populated live from HA attributes on each state
58563
- * push — consistent with `options` and the sensor-unit approach.
59031
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59032
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59033
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58564
59034
  */
58565
59035
  var ControlStatusSchema = object({
58566
59036
  kind: _enum([
@@ -58572,39 +59042,17 @@ var ControlStatusSchema = object({
58572
59042
  /** Current value. Numeric kind → number; select/text/datetime → string.
58573
59043
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58574
59044
  * or time-only — HA accepts all three forms). */
58575
- value: union([number(), string()]),
59045
+ value: union([number().meta(readingMeta("device")), string()]),
58576
59046
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58577
59047
  options: array(string()),
58578
59048
  /** Ms epoch when the slice was last updated. */
58579
59049
  lastChangedAt: number(),
58580
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58581
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58582
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58583
- unit: string().optional(),
58584
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58585
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58586
- min: number().optional(),
58587
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58588
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58589
- max: number().optional(),
58590
- /** Step increment. Populated live from HA `attributes.step`.
58591
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58592
- step: number().optional(),
58593
- /** Suggested decimal places for numeric display. Populated live from HA
58594
- * `attributes.suggested_display_precision`, or derived from `step` when the
58595
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58596
- * other kinds. Falls back to auto-formatting when absent. */
58597
- precision: number().int().min(0).max(10).optional(),
58598
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58599
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58600
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58601
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58602
- * `has_date`/`has_time` attributes on each push. */
58603
- format: _enum([
58604
- "date",
58605
- "time",
58606
- "datetime"
58607
- ]).optional()
59050
+ /** Minimum allowed value, in the device quantity's base unit. */
59051
+ min: number().optional().meta(readingMeta("device")),
59052
+ /** Maximum allowed value, in the device quantity's base unit. */
59053
+ max: number().optional().meta(readingMeta("device")),
59054
+ /** Step increment, in the device quantity's base unit. */
59055
+ step: number().optional().meta(readingMeta("device"))
58608
59056
  });
58609
59057
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58610
59058
  object({
@@ -58670,9 +59118,9 @@ var CoverStatusSchema = object({
58670
59118
  ]),
58671
59119
  /** 0 = fully closed, 100 = fully open. Null when the device has
58672
59120
  * no intermediate position surface. */
58673
- position: number().min(0).max(100).nullable(),
59121
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58674
59122
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58675
- tiltPosition: number().min(0).max(100).nullable(),
59123
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58676
59124
  /** Ms epoch when the slice was last updated. */
58677
59125
  lastChangedAt: number()
58678
59126
  });
@@ -58775,9 +59223,9 @@ var NormalizedRangeSchema$1 = object({
58775
59223
  var DayNightStatusSchema = object({
58776
59224
  mode: DayNightModeSchema,
58777
59225
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58778
- sensitivity: number().optional(),
59226
+ sensitivity: number().optional().meta(readingMeta("none")),
58779
59227
  /** Delay before the IR-cut filter flips, in seconds. */
58780
- switchDelaySec: number().optional(),
59228
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58781
59229
  lastFetchedAt: number()
58782
59230
  });
58783
59231
  /**
@@ -58995,11 +59443,11 @@ var DishwasherStatusSchema = object({
58995
59443
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58996
59444
  availablePrograms: array(string()),
58997
59445
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58998
- remainingSeconds: number().int().nonnegative().nullable(),
59446
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58999
59447
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59000
- delayedStartSeconds: number().int().nonnegative().nullable(),
59448
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59001
59449
  /** Cycle completion 0..100, or null when not applicable. */
59002
- progressPercent: number().min(0).max(100).nullable(),
59450
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59003
59451
  /** True when the door is reported open; null when the door state is not known. */
59004
59452
  doorOpen: boolean().nullable(),
59005
59453
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59023,7 +59471,7 @@ var DishwasherStatusSchema = object({
59023
59471
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59024
59472
  machineCareDue: boolean().nullable(),
59025
59473
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59026
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59474
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59027
59475
  /** Option states: null when the appliance does not expose the option or has not said. */
59028
59476
  halfLoad: boolean().nullable(),
59029
59477
  hygienePlus: boolean().nullable(),
@@ -59038,7 +59486,7 @@ var DishwasherStatusSchema = object({
59038
59486
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59039
59487
  activeEvents: array(string()),
59040
59488
  /** Numeric error code when the appliance reports one; null otherwise. */
59041
- errorCode: number().nullable(),
59489
+ errorCode: number().nullable().meta(readingMeta("none")),
59042
59490
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59043
59491
  errorLabel: string().nullable(),
59044
59492
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59269,7 +59717,7 @@ var DoorbellStatusSchema = object({
59269
59717
  /** Ms epoch of the last press. null = never observed since this provider started. */
59270
59718
  lastPressedAt: number().nullable(),
59271
59719
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59272
- pressCountSinceStart: number()
59720
+ pressCountSinceStart: number().meta(readingMeta("count"))
59273
59721
  });
59274
59722
  var DoorbellPressEventSchema = object({
59275
59723
  deviceId: number(),
@@ -59334,17 +59782,7 @@ onPressed: {
59334
59782
  history: "none",
59335
59783
  customize: doorbellCustomize
59336
59784
  };
59337
- /**
59338
- * Enum-state sensor — a string value picked from a finite option set.
59339
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59340
- * states, weather conditions, contact-source channels, mode strings).
59341
- * The option set is stable across observations; it is persisted in the
59342
- * device config blob (via the control slice at adoption) and is NOT
59343
- * stored in `sourceInfo`.
59344
- */
59345
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59346
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59347
- var EnumSensorDateTimeFormatSchema = _enum([
59785
+ _enum([
59348
59786
  "date",
59349
59787
  "time",
59350
59788
  "datetime"
@@ -59352,12 +59790,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59352
59790
  var EnumSensorStatusSchema = object({
59353
59791
  value: string(),
59354
59792
  /**
59355
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59356
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59357
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59358
- * a time-only sensor → `'time'`.
59793
+ * Set when `value` is a timestamp rather than an enum state, so history
59794
+ * does not learn ISO strings as states. The display shape (date / time /
59795
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59359
59796
  */
59360
- format: EnumSensorDateTimeFormatSchema.optional(),
59797
+ timestamp: boolean().optional(),
59361
59798
  /** Ms epoch when the slice was last updated. */
59362
59799
  lastFetchedAt: number()
59363
59800
  });
@@ -59384,11 +59821,11 @@ var enumSensorCapability = {
59384
59821
  * History (D850): the value as an `enum` series (an appliance's program or
59385
59822
  * operation state, an HVAC action). The option set is per device, so the
59386
59823
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59387
- * `format` carries a timestamp, not a state, and is not recorded.
59824
+ * `timestamp` carries a clock, not a state, and is not recorded.
59388
59825
  */
59389
59826
  history: { fields: { value: {
59390
59827
  kind: "enum",
59391
- skipWhenSet: "format"
59828
+ skipWhenSet: "timestamp"
59392
59829
  } } },
59393
59830
  /** Clock fields: written, but excluded from the compare that decides
59394
59831
  * whether persisting is worth a SQLite commit. */
@@ -59408,13 +59845,13 @@ var EventFireSchema = object({
59408
59845
  deviceId: number(),
59409
59846
  eventType: string(),
59410
59847
  data: record(string(), unknown()).nullable(),
59411
- timestamp: number(),
59412
- seq: number()
59848
+ timestamp: number().meta(readingMeta("none")),
59849
+ seq: number().meta(readingMeta("count"))
59413
59850
  });
59414
59851
  var EventEmitterStatusSchema = object({
59415
59852
  eventTypes: array(string()),
59416
59853
  lastEvent: EventFireSchema.nullable(),
59417
- eventCountSinceStart: number()
59854
+ eventCountSinceStart: number().meta(readingMeta("count"))
59418
59855
  });
59419
59856
  var eventEmitterCapability = {
59420
59857
  name: "event-emitter",
@@ -59827,10 +60264,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59827
60264
  var FanControlStatusSchema = object({
59828
60265
  /** Active speed as 0..100 inclusive. Null when the device has no
59829
60266
  * speed surface (single-speed fan). */
59830
- percentage: number().min(0).max(100).nullable(),
60267
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59831
60268
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59832
60269
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59833
- percentageStep: number().positive().optional(),
60270
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59834
60271
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59835
60272
  * — empty string when no preset is active or supported. */
59836
60273
  preset: string(),
@@ -59951,7 +60388,7 @@ var FeatureProbeStatusSchema = object({
59951
60388
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59952
60389
  model: string().nullable(),
59953
60390
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59954
- channelCount: number().nullable(),
60391
+ channelCount: number().nullable().meta(readingMeta("count")),
59955
60392
  /**
59956
60393
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59957
60394
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60074,9 +60511,91 @@ var gasCapability = {
60074
60511
  * whether persisting is worth a SQLite commit. */
60075
60512
  volatileStateFields: ["lastChangedAt"]
60076
60513
  };
60514
+ var GroupCommandsSchema = object({
60515
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60516
+ results: array(object({
60517
+ deviceId: number().int().nonnegative(),
60518
+ cap: string().min(1),
60519
+ method: string().min(1),
60520
+ ok: boolean(),
60521
+ error: string().optional(),
60522
+ /** Ms epoch of the attempt. */
60523
+ at: number()
60524
+ })) });
60525
+ var groupCommandsCapability = {
60526
+ name: "group-commands",
60527
+ scope: "device",
60528
+ deviceNative: true,
60529
+ mode: "singleton",
60530
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60531
+ methods: {},
60532
+ status: {
60533
+ schema: GroupCommandsSchema,
60534
+ kind: "push"
60535
+ },
60536
+ runtimeState: GroupCommandsSchema,
60537
+ /**
60538
+ * Runtime-state durability: **session** — the last attempt. A restored
60539
+ * failure would outlive the command that caused it.
60540
+ *
60541
+ * See `RuntimeStateDurability`. Enforced by
60542
+ * `scripts/check-runtime-state-durability.ts`.
60543
+ */
60544
+ durability: "session",
60545
+ history: "none"
60546
+ };
60547
+ /**
60548
+ * How many members of a helper group answered (D944). Counts are integers,
60549
+ * not measurements: zero included is a real count, never a stand-in reading.
60550
+ * The reading itself stays on the output capability. History is none — the
60551
+ * series that later statistics read is the output cap's own history block.
60552
+ */
60553
+ var GroupStatusSchema = object({
60554
+ /** Members whose current value entered the aggregate. */
60555
+ included: number().int().nonnegative().meta(readingMeta("count", {
60556
+ precision: 0,
60557
+ label: "Reporting"
60558
+ })),
60559
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60560
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60561
+ precision: 0,
60562
+ label: "Excluded"
60563
+ })),
60564
+ /** `included` + `excluded`. */
60565
+ total: number().int().nonnegative().meta(readingMeta("count", {
60566
+ precision: 0,
60567
+ label: "Members"
60568
+ }))
60569
+ });
60570
+ var groupStatusCapability = {
60571
+ name: "group-status",
60572
+ scope: "device",
60573
+ deviceNative: true,
60574
+ mode: "singleton",
60575
+ deviceTypes: [
60576
+ DeviceType.Sensor,
60577
+ DeviceType.Switch,
60578
+ DeviceType.Light
60579
+ ],
60580
+ methods: {},
60581
+ status: {
60582
+ schema: GroupStatusSchema,
60583
+ kind: "push"
60584
+ },
60585
+ runtimeState: GroupStatusSchema,
60586
+ /**
60587
+ * Runtime-state durability: **session** — recomputed from members on the
60588
+ * next source event; a restored count would outlive the readings it named.
60589
+ *
60590
+ * See `RuntimeStateDurability`. Enforced by
60591
+ * `scripts/check-runtime-state-durability.ts`.
60592
+ */
60593
+ durability: "session",
60594
+ history: "none"
60595
+ };
60077
60596
  var HeatingValveStatusSchema = object({
60078
60597
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60079
- position: number().min(0).max(100).nullable(),
60598
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60080
60599
  /** Ms epoch when the slice was last updated (push or poll). */
60081
60600
  lastFetchedAt: number()
60082
60601
  });
@@ -60119,9 +60638,9 @@ var HumidifierStatusSchema = object({
60119
60638
  /** Whether the humidifier is currently on. */
60120
60639
  on: boolean(),
60121
60640
  /** Current measured relative humidity (0..100). Null when not reported. */
60122
- currentHumidity: number().min(0).max(100).nullable(),
60641
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60123
60642
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60124
- targetHumidity: number().min(0).max(100).nullable(),
60643
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60125
60644
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60126
60645
  * device has no mode surface. */
60127
60646
  mode: string().nullable(),
@@ -60131,9 +60650,9 @@ var HumidifierStatusSchema = object({
60131
60650
  * `idle` / `off`). Null when the device doesn't report it. */
60132
60651
  action: string().nullable(),
60133
60652
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60134
- minHumidity: number().nullable(),
60653
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60135
60654
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60136
- maxHumidity: number().nullable(),
60655
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60137
60656
  /** Ms epoch when the slice was last updated. */
60138
60657
  lastChangedAt: number()
60139
60658
  });
@@ -60203,22 +60722,13 @@ var humidifierCapability = {
60203
60722
  * `sourceInfo`.
60204
60723
  */
60205
60724
  var HumiditySensorStatusSchema = object({
60206
- /** Current relative humidity, 0..100. */
60207
- percent: number().min(0).max(100).meta({
60208
- unit: "%",
60725
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60726
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60727
+ precision: 0,
60209
60728
  label: "Humidity"
60210
- }),
60729
+ })),
60211
60730
  /** Ms epoch when the slice was last updated. */
60212
- lastFetchedAt: number(),
60213
- /** Live display unit from the upstream source (e.g. HA
60214
- * `attributes.unit_of_measurement`). The UI prefers this over the
60215
- * role's canonical unit. Absent → fall back to the canonical unit. */
60216
- unit: string().optional(),
60217
- /** Suggested decimal places for numeric display.
60218
- * Populated live from the upstream source when provided (e.g. HA
60219
- * `attributes.suggested_display_precision`). Falls back to
60220
- * auto-formatting when absent. */
60221
- precision: number().int().min(0).max(10).optional()
60731
+ lastFetchedAt: number()
60222
60732
  });
60223
60733
  var humiditySensorCapability = {
60224
60734
  name: "humidity-sensor",
@@ -60249,12 +60759,7 @@ var humiditySensorCapability = {
60249
60759
  } } },
60250
60760
  /** Clock fields: written, but excluded from the compare that decides
60251
60761
  * whether persisting is worth a SQLite commit. */
60252
- volatileStateFields: ["lastFetchedAt"],
60253
- customize: {
60254
- version: 1,
60255
- advanced: "derive",
60256
- targets: []
60257
- }
60762
+ volatileStateFields: ["lastFetchedAt"]
60258
60763
  };
60259
60764
  /**
60260
60765
  * Image display cap. Models a single still image exposed by an integration —
@@ -60275,7 +60780,7 @@ var ImageStatusSchema = object({
60275
60780
  * entity exposes no `entity_picture` (yet). */
60276
60781
  url: string().nullable(),
60277
60782
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60278
- lastUpdated: number().nullable()
60783
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60279
60784
  });
60280
60785
  var imageCapability = {
60281
60786
  name: "image",
@@ -60363,19 +60868,19 @@ var NormalizedRangeSchema = object({
60363
60868
  */
60364
60869
  var ImageSettingsStatusSchema = object({
60365
60870
  /** Normalized 0–100. */
60366
- brightness: number().optional(),
60871
+ brightness: number().optional().meta(readingMeta("none")),
60367
60872
  /** Normalized 0–100. */
60368
- contrast: number().optional(),
60873
+ contrast: number().optional().meta(readingMeta("none")),
60369
60874
  /** Normalized 0–100. */
60370
- saturation: number().optional(),
60875
+ saturation: number().optional().meta(readingMeta("none")),
60371
60876
  /** Normalized 0–100. */
60372
- sharpness: number().optional(),
60877
+ sharpness: number().optional().meta(readingMeta("none")),
60373
60878
  mirror: boolean().optional(),
60374
60879
  flip: boolean().optional(),
60375
60880
  rotate: ImageRotateSchema.optional(),
60376
60881
  whiteBalance: WhiteBalanceModeSchema.optional(),
60377
60882
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60378
- warmth: number().optional(),
60883
+ warmth: number().optional().meta(readingMeta("none")),
60379
60884
  exposureMode: ExposureModeSchema.optional(),
60380
60885
  backlightMode: BacklightModeSchema.optional(),
60381
60886
  lastFetchedAt: number()
@@ -60654,10 +61159,10 @@ var IntercomAbilitySchema = object({
60654
61159
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60655
61160
  codecs: array(string()),
60656
61161
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60657
- sampleRate: number(),
61162
+ sampleRate: number().meta(readingMeta("none")),
60658
61163
  duplex: _enum(["half", "full"]),
60659
61164
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60660
- maxBacklogMs: number()
61165
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60661
61166
  });
60662
61167
  var IntercomStatusSchema = object({
60663
61168
  /** True while a talk session is open. */
@@ -60884,13 +61389,13 @@ var LawnMowerControlStatusSchema = object({
60884
61389
  activity: LawnMowerActivitySchema,
60885
61390
  /** 0..100 battery percentage. Null when the device has no battery
60886
61391
  * reading. */
60887
- batteryLevel: number().min(0).max(100).nullable(),
61392
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60888
61393
  /** 0..100 mowing-completion percentage of the current task, or null when no
60889
61394
  * task is active / progress is unavailable. */
60890
- progressPercent: number().min(0).max(100).nullable(),
61395
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60891
61396
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60892
61397
  * or null when unknown. */
60893
- currentCode: number().nullable(),
61398
+ currentCode: number().nullable().meta(readingMeta("none")),
60894
61399
  /** Human label for {@link currentCode}, or null when undecodable. */
60895
61400
  currentCodeLabel: string().nullable(),
60896
61401
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61423,7 +61928,7 @@ var MediaPlayerStatusSchema = object({
61423
61928
  state: MediaPlayerStateSchema,
61424
61929
  /** Volume as 0..100 inclusive. Null when the device has no volume
61425
61930
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61426
- volumeLevel: number().min(0).max(100).nullable(),
61931
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61427
61932
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61428
61933
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61429
61934
  isMuted: boolean().nullable(),
@@ -61436,10 +61941,10 @@ var MediaPlayerStatusSchema = object({
61436
61941
  currentMedia: MediaInfoSchema.nullable(),
61437
61942
  /** Current playback position in ms. Null when not seekable or
61438
61943
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61439
- positionMs: number().int().nonnegative().nullable(),
61944
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61440
61945
  /** Total duration of the current media in ms. Null when unknown
61441
61946
  * (live stream) or nothing is playing. */
61442
- durationMs: number().int().nonnegative().nullable(),
61947
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61443
61948
  /** Shuffle toggle. Null when no shuffle surface.
61444
61949
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61445
61950
  shuffle: boolean().nullable(),
@@ -61748,7 +62253,7 @@ var motionCustomize = {
61748
62253
  eventName: "onMotionChanged"
61749
62254
  },
61750
62255
  deviceTypes: [DeviceType.Camera],
61751
- apply: "replace",
62256
+ apply: "add",
61752
62257
  applyAuthority: {
61753
62258
  kind: "camera-sensors",
61754
62259
  timeoutField: "motionCooldownMs"
@@ -61820,7 +62325,7 @@ var MotionStatusSchema = object({
61820
62325
  * it, every provider seeded it at boot, and every session left it
61821
62326
  * `detected: true` after its detach.
61822
62327
  */
61823
- autoClearAfterMs: number().nullable()
62328
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61824
62329
  });
61825
62330
  /**
61826
62331
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61857,12 +62362,18 @@ var motionCapability = {
61857
62362
  methods: {
61858
62363
  /**
61859
62364
  * Pull the current motion state synchronously. Convenience shortcut
61860
- * for consumers that don't need the full status object; equivalent
61861
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61862
- * `getStatus` once the auto-injected status surface lands in every
61863
- * consumer.
62365
+ * for consumers that don't need the full status object. A composed
62366
+ * group answers from its slice (D944): unknown throws, it is not off.
61864
62367
  */
61865
62368
  isDetected: method(object({ deviceId: number() }), boolean()) },
62369
+ /**
62370
+ * A composed motion group serves `isDetected` from `detected` (D944).
62371
+ * Without this the cap stays uncomposable (`has-commands`).
62372
+ */
62373
+ composedMethods: { isDetected: {
62374
+ kind: "slice",
62375
+ field: "detected"
62376
+ } },
61866
62377
  events: {
61867
62378
  /**
61868
62379
  * Fires every time the runner transitions a camera between
@@ -62002,7 +62513,7 @@ var MotionZoneRegionSchema = object({
62002
62513
  * the grid region(s). */
62003
62514
  var MotionZoneStatusSchema = object({
62004
62515
  enabled: boolean(),
62005
- sensitivity: number(),
62516
+ sensitivity: number().meta(readingMeta("none")),
62006
62517
  /** Grid region(s). Today exactly one `grid` shape. */
62007
62518
  regions: array(MotionZoneRegionSchema),
62008
62519
  lastFetchedAt: number()
@@ -62372,7 +62883,7 @@ var NavigationStatusSchema = object({
62372
62883
  following: boolean(),
62373
62884
  flash: boolean(),
62374
62885
  lightMode: NavigationLightModeSchema,
62375
- lightLevel: number().min(40).max(100),
62886
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62376
62887
  /** Ms epoch when the slice was last updated. */
62377
62888
  lastChangedAt: number()
62378
62889
  });
@@ -62506,13 +63017,13 @@ var NetworkLinkStatusSchema = object({
62506
63017
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62507
63018
  * SKIP a null rather than coerce it.
62508
63019
  */
62509
- signalPercent: number().min(0).max(100).nullable(),
63020
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62510
63021
  /** Raw received signal strength in dBm, when the firmware reports one. */
62511
- rssiDbm: number().optional(),
63022
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62512
63023
  /** Network name of a wireless link, when the firmware reports it. */
62513
63024
  ssid: string().optional(),
62514
63025
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62515
- lastUpdated: number()
63026
+ lastUpdated: number().meta(readingMeta("none"))
62516
63027
  });
62517
63028
  var networkLinkCapability = {
62518
63029
  name: "network-link",
@@ -62897,7 +63408,7 @@ var NotifierStatusSchema = object({
62897
63408
  lastError: string().nullable(),
62898
63409
  /** Number of deliveries currently buffered server-side (rate-limited
62899
63410
  * notifiers may queue). 0 when the provider sends synchronously. */
62900
- queueDepth: number().int().nonnegative(),
63411
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62901
63412
  /** Per-feature capability matrix — authoritative source for the
62902
63413
  * compose-form field gating. */
62903
63414
  supports: NotifierSupportsSchema
@@ -62989,11 +63500,9 @@ onSent: { data: object({
62989
63500
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62990
63501
  * for: water flow, distance, weight, frequency, signal strength, …).
62991
63502
  *
62992
- * The `unit` and `precision` fields carry the live display metadata that
62993
- * the upstream source provides with each state push (HA
62994
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62995
- * The UI reads them directly from the slice — they are the single source
62996
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63503
+ * The quantity is not on this slice. It is chosen when the device is
63504
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63505
+ * that quantity's base unit. `null` is a refused sample (D393).
62997
63506
  *
62998
63507
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62999
63508
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63002,15 +63511,8 @@ onSent: { data: object({
63002
63511
  * numeric values land here.
63003
63512
  */
63004
63513
  var NumericSensorStatusSchema = object({
63005
- value: number(),
63006
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63007
- * Populated live from the upstream source on each state push.
63008
- * Absent for unitless sensors. */
63009
- unit: string().optional(),
63010
- /** Suggested decimal places for numeric display.
63011
- * Populated live from the upstream source when provided.
63012
- * Falls back to auto-formatting when absent. */
63013
- precision: number().int().min(0).max(10).optional(),
63514
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63515
+ value: number().nullable().meta(readingMeta("device")),
63014
63516
  /** Ms epoch when the slice was last updated. */
63015
63517
  lastFetchedAt: number()
63016
63518
  });
@@ -63034,11 +63536,11 @@ var numericSensorCapability = {
63034
63536
  */
63035
63537
  durability: "restored",
63036
63538
  /**
63037
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63539
+ * History (D850): recorded series. The unit is the device quantity's base,
63540
+ * resolved when the series is read — the cap has no single unit.
63038
63541
  */
63039
63542
  history: { fields: { value: {
63040
63543
  kind: "gauge",
63041
- unitFrom: "unit",
63042
63544
  minIntervalMs: 3e4
63043
63545
  } } },
63044
63546
  /** Clock fields: written, but excluded from the compare that decides
@@ -63330,23 +63832,26 @@ var PetFeederStatusSchema = object({
63330
63832
  /** Food currently in the bowl (grams). Null when the device has not
63331
63833
  * reported a reading yet. On dual-hopper models this is the combined
63332
63834
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63333
- foodLevel: number().nullable(),
63835
+ foodLevel: number().nullable().meta(readingMeta("none")),
63334
63836
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63335
63837
  * single-hopper models. */
63336
- food1: number().nullable(),
63838
+ food1: number().nullable().meta(readingMeta("none")),
63337
63839
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63338
63840
  * single-hopper models. */
63339
- food2: number().nullable(),
63841
+ food2: number().nullable().meta(readingMeta("none")),
63340
63842
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63341
63843
  * (`device_class: problem`, on = low). True when the bowl is empty /
63342
63844
  * below the feeder's low threshold. */
63343
63845
  lowFood: boolean(),
63344
63846
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63345
63847
  * device has no battery reading. */
63346
- batteryPower: number().min(0).max(100).nullable(),
63347
- /** Days of desiccant life remaining. Null when the model has no
63348
- * desiccant sensor. */
63349
- desiccantLeftDays: number().nullable(),
63848
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63849
+ /**
63850
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63851
+ * Providers convert from the days the feeder reports. Null when the
63852
+ * model has no desiccant sensor.
63853
+ */
63854
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63350
63855
  /** True while a feed is in progress. */
63351
63856
  feeding: boolean(),
63352
63857
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63357,7 +63862,7 @@ var PetFeederStatusSchema = object({
63357
63862
  * with `errorCode` for consumers that want the raw integer. */
63358
63863
  error: string().nullable(),
63359
63864
  /** Raw device error code (0 / null = no error). */
63360
- errorCode: number().nullable(),
63865
+ errorCode: number().nullable().meta(readingMeta("none")),
63361
63866
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63362
63867
  isDualHopper: boolean(),
63363
63868
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63367,7 +63872,7 @@ var PetFeederStatusSchema = object({
63367
63872
  /** Play a chime when dispensing. */
63368
63873
  feedSound: boolean(),
63369
63874
  /** Speaker / prompt volume level (device-scaled integer). */
63370
- volume: number(),
63875
+ volume: number().meta(readingMeta("none")),
63371
63876
  /** Ms epoch when the slice was last refreshed from the cloud. */
63372
63877
  lastFetchedAt: number()
63373
63878
  });
@@ -63486,7 +63991,7 @@ var petFeederCapability = {
63486
63991
  },
63487
63992
  desiccantLeftDays: {
63488
63993
  kind: "gauge",
63489
- unit: "days"
63994
+ unit: "s"
63490
63995
  }
63491
63996
  } }
63492
63997
  };
@@ -63887,36 +64392,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63887
64392
  */
63888
64393
  var PowerMeterStatusSchema = object({
63889
64394
  /** Instantaneous power draw in watts. */
63890
- watts: number().optional().meta({
63891
- unit: "W",
63892
- label: "Power"
63893
- }),
64395
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63894
64396
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63895
- kwhTotal: number().optional().meta({
63896
- unit: "kWh",
63897
- label: "Energy"
63898
- }),
64397
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63899
64398
  /** Voltage in volts. */
63900
- volts: number().optional(),
64399
+ volts: number().optional().meta(readingMeta("voltage")),
63901
64400
  /** Current in amperes. */
63902
- amps: number().optional(),
63903
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63904
- va: number().optional(),
64401
+ amps: number().optional().meta(readingMeta("current")),
64402
+ /** Apparent power in volt-amperes. */
64403
+ va: number().optional().meta(readingMeta("apparent-power")),
63905
64404
  /** Power factor (0..1) if reported. */
63906
- powerFactor: number().min(0).max(1).optional(),
64405
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63907
64406
  /** Ms epoch when the slice was last updated. */
63908
- lastFetchedAt: number(),
63909
- /** Live display unit of the single metric this slice carries (e.g. HA
63910
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63911
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63912
- * unit per slice is unambiguous. The UI prefers this over the role's
63913
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63914
- unit: string().optional(),
63915
- /** Suggested decimal places for numeric display.
63916
- * Populated live from the upstream source when provided (e.g. HA
63917
- * `attributes.suggested_display_precision`). Falls back to
63918
- * auto-formatting when absent. */
63919
- precision: number().int().min(0).max(10).optional()
64407
+ lastFetchedAt: number()
63920
64408
  });
63921
64409
  var powerMeterCapability = {
63922
64410
  name: "power-meter",
@@ -63980,12 +64468,7 @@ var powerMeterCapability = {
63980
64468
  } },
63981
64469
  /** Clock fields: written, but excluded from the compare that decides
63982
64470
  * whether persisting is worth a SQLite commit. */
63983
- volatileStateFields: ["lastFetchedAt"],
63984
- customize: {
63985
- version: 1,
63986
- advanced: "derive",
63987
- targets: []
63988
- }
64471
+ volatileStateFields: ["lastFetchedAt"]
63989
64472
  };
63990
64473
  /**
63991
64474
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64005,11 +64488,11 @@ var powerMeterCapability = {
64005
64488
  */
64006
64489
  var GpsLocationSchema = object({
64007
64490
  /** Latitude in decimal degrees, -90..90. */
64008
- latitude: number().min(-90).max(90),
64491
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64009
64492
  /** Longitude in decimal degrees, -180..180. */
64010
- longitude: number().min(-180).max(180),
64493
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64011
64494
  /** Reported accuracy in meters (lower = better). */
64012
- accuracyMeters: number().nonnegative()
64495
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64013
64496
  });
64014
64497
  var PresenceStatusSchema = object({
64015
64498
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64021,7 +64504,7 @@ var PresenceStatusSchema = object({
64021
64504
  gps: GpsLocationSchema.nullable(),
64022
64505
  /** Optional battery level of the tracking device (0..100). Null
64023
64506
  * when not reported. */
64024
- batteryPercent: number().min(0).max(100).nullable(),
64507
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64025
64508
  /** Ms epoch when the slice was last updated. */
64026
64509
  lastChangedAt: number()
64027
64510
  });
@@ -64071,19 +64554,10 @@ var presenceCapability = {
64071
64554
  * `sourceInfo`.
64072
64555
  */
64073
64556
  var PressureSensorStatusSchema = object({
64074
- /** Current pressure in hPa. */
64075
- hpa: number(),
64557
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64558
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64076
64559
  /** Ms epoch when the slice was last updated. */
64077
- lastFetchedAt: number(),
64078
- /** Live display unit from the upstream source (e.g. HA
64079
- * `attributes.unit_of_measurement`). The UI prefers this over the
64080
- * role's canonical unit. Absent → fall back to the canonical unit. */
64081
- unit: string().optional(),
64082
- /** Suggested decimal places for numeric display.
64083
- * Populated live from the upstream source when provided (e.g. HA
64084
- * `attributes.suggested_display_precision`). Falls back to
64085
- * auto-formatting when absent. */
64086
- precision: number().int().min(0).max(10).optional()
64560
+ lastFetchedAt: number()
64087
64561
  });
64088
64562
  var pressureSensorCapability = {
64089
64563
  name: "pressure-sensor",
@@ -64497,8 +64971,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64497
64971
  }), _void(), { kind: "mutation" });
64498
64972
  var PtzAutotrackSettingsSchema = object({
64499
64973
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64500
- stopDelaySeconds: number().int().min(0).max(300),
64501
- disappearDelaySeconds: number().int().min(0).max(300)
64974
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64975
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64502
64976
  });
64503
64977
  /**
64504
64978
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64652,7 +65126,7 @@ var RunningStateStatusSchema = object({
64652
65126
  /** null = unknown — never coerced to false (D393). */
64653
65127
  running: boolean().nullable(),
64654
65128
  /** Epoch ms the current boolean began; null while `running` is null. */
64655
- since: number().nullable(),
65129
+ since: number().nullable().meta(readingMeta("none")),
64656
65130
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64657
65131
  phase: string().nullable(),
64658
65132
  /** null until a reading has been published (D393). */
@@ -64667,7 +65141,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64667
65141
  /** Generic Customize target for the running boolean (D936). */
64668
65142
  var runningCustomize = {
64669
65143
  version: 1,
64670
- advanced: "derive",
64671
65144
  targets: [{
64672
65145
  id: "running",
64673
65146
  cardinality: "scalar",
@@ -64687,7 +65160,7 @@ var runningCustomize = {
64687
65160
  DeviceType.Thermostat,
64688
65161
  DeviceType.PetFeeder
64689
65162
  ],
64690
- apply: "add-or-replace",
65163
+ apply: "add",
64691
65164
  path: "running",
64692
65165
  sources: [
64693
65166
  {
@@ -65951,9 +66424,9 @@ var RecordTriggerSchema = _enum([
65951
66424
  */
65952
66425
  var RecordWindowSchema = object({
65953
66426
  trigger: RecordTriggerSchema,
65954
- day: number().int().min(0).max(6),
65955
- startMinute: number().int().min(0).max(1439),
65956
- endMinute: number().int().min(1).max(1440)
66427
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66428
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66429
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65957
66430
  });
65958
66431
  /** Status of one physical volume, as the camera itself describes it. */
65959
66432
  var OnboardStorageVolumeSchema = object({
@@ -66029,10 +66502,10 @@ var RecordingOnboardStatusSchema = object({
66029
66502
  /** Master "record to the card at all" switch. */
66030
66503
  enabled: boolean().nullable(),
66031
66504
  overwriteWhenFull: boolean().nullable(),
66032
- preRecordSec: number().nullable(),
66033
- postRecordSec: number().nullable(),
66505
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66506
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66034
66507
  /** Length of one recorded file, in minutes. */
66035
- segmentMinutes: number().nullable(),
66508
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66036
66509
  /** The primary track's weekly windows, flattened. */
66037
66510
  windows: array(RecordWindowSchema),
66038
66511
  /**
@@ -66048,7 +66521,7 @@ var RecordingOnboardStatusSchema = object({
66048
66521
  * provider reporting a non-zero count also reports the schedule as not
66049
66522
  * writable.
66050
66523
  */
66051
- unreadableWindows: number(),
66524
+ unreadableWindows: number().meta(readingMeta("count")),
66052
66525
  /**
66053
66526
  * The camera is scheduled to record and has NO usable storage.
66054
66527
  *
@@ -66893,7 +67366,7 @@ var CamStreamDescriptorSchema = object({
66893
67366
  url: string().optional(),
66894
67367
  codec: string().optional(),
66895
67368
  resolution: CamStreamResolutionSchema.optional(),
66896
- fps: number().positive().optional(),
67369
+ fps: number().positive().optional().meta(readingMeta("rate")),
66897
67370
  label: string().optional(),
66898
67371
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66899
67372
  deviceFeatures: array(string()).optional(),
@@ -66980,18 +67453,18 @@ var StreamProfileSchema = _enum([
66980
67453
  ]);
66981
67454
  /** The current encoding config of one stream profile. */
66982
67455
  var StreamProfileConfigSchema = object({
66983
- width: number(),
66984
- height: number(),
67456
+ width: number().meta(readingMeta("none")),
67457
+ height: number().meta(readingMeta("none")),
66985
67458
  codec: _enum(["h264", "h265"]),
66986
- framerate: number(),
66987
- bitrate: number(),
67459
+ framerate: number().meta(readingMeta("rate")),
67460
+ bitrate: number().meta(readingMeta("bitrate")),
66988
67461
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66989
67462
  encoderProfile: _enum([
66990
67463
  "high",
66991
67464
  "main",
66992
67465
  "baseline"
66993
67466
  ]).optional(),
66994
- gop: number().optional(),
67467
+ gop: number().optional().meta(readingMeta("none")),
66995
67468
  /**
66996
67469
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66997
67470
  *
@@ -67212,12 +67685,7 @@ var switchCapability = {
67212
67685
  label: "On",
67213
67686
  kind: "boolean"
67214
67687
  }
67215
- }] },
67216
- customize: {
67217
- version: 1,
67218
- advanced: "derive",
67219
- targets: []
67220
- }
67688
+ }] }
67221
67689
  };
67222
67690
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67223
67691
  function hasControlCharacter(value) {
@@ -67800,35 +68268,21 @@ var tamperCapability = {
67800
68268
  * entries with `device_class: temperature` and any future native
67801
68269
  * thermometer.
67802
68270
  *
67803
- * Unit normalisation: providers convert to Celsius before storing.
67804
- * The slice value is always Celsius so cross-cap aggregators
67805
- * (climate-control's `currentTemp`, energy analytics) can compose
67806
- * without per-source unit fixups. The canonical display unit (`°C`) is
67807
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67808
- * `sourceInfo`.
68271
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68272
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67809
68273
  *
67810
68274
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67811
68275
  * frozen feed (provider hung) distinct from a "temperature hasn't
67812
68276
  * changed" steady state.
67813
68277
  */
67814
68278
  var TemperatureSensorStatusSchema = object({
67815
- /** Current temperature in Celsius. */
67816
- celsius: number().meta({
67817
- unit: "°C",
68279
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68280
+ celsius: number().nullable().meta(readingMeta("temperature", {
68281
+ precision: 1,
67818
68282
  label: "Temperature"
67819
- }),
68283
+ })),
67820
68284
  /** Ms epoch when the slice was last updated (push or poll). */
67821
- lastFetchedAt: number(),
67822
- /** Live display unit from the upstream source (e.g. HA
67823
- * `attributes.unit_of_measurement`). The UI prefers this over the
67824
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67825
- * Absent → the UI falls back to the role's canonical unit. */
67826
- unit: string().optional(),
67827
- /** Suggested decimal places for numeric display.
67828
- * Populated live from the upstream source when provided (e.g. HA
67829
- * `attributes.suggested_display_precision`). Falls back to
67830
- * auto-formatting when absent. */
67831
- precision: number().int().min(0).max(10).optional()
68285
+ lastFetchedAt: number()
67832
68286
  });
67833
68287
  var temperatureSensorCapability = {
67834
68288
  name: "temperature-sensor",
@@ -67859,12 +68313,7 @@ var temperatureSensorCapability = {
67859
68313
  } } },
67860
68314
  /** Clock fields: written, but excluded from the compare that decides
67861
68315
  * whether persisting is worth a SQLite commit. */
67862
- volatileStateFields: ["lastFetchedAt"],
67863
- customize: {
67864
- version: 1,
67865
- advanced: "derive",
67866
- targets: []
67867
- }
68316
+ volatileStateFields: ["lastFetchedAt"]
67868
68317
  };
67869
68318
  /**
67870
68319
  * toast — system-scoped singleton capability that streams toast
@@ -68147,16 +68596,16 @@ var VacuumControlStatusSchema = object({
68147
68596
  ]),
68148
68597
  /** 0..100 battery percentage. Null when the device has no battery
68149
68598
  * reading. */
68150
- batteryLevel: number().min(0).max(100).nullable(),
68599
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68151
68600
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68152
68601
  * the vacuum has no speed control. */
68153
68602
  fanSpeed: string().nullable(),
68154
68603
  /** Speed tokens the hardware accepts — drives the UI selector. */
68155
68604
  availableFanSpeeds: array(string()),
68156
68605
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68157
- progressPercent: number().min(0).max(100).nullable(),
68606
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68158
68607
  /** Current error code (0 / null = no error). */
68159
- errorCode: number().nullable(),
68608
+ errorCode: number().nullable().meta(readingMeta("none")),
68160
68609
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68161
68610
  errorLabel: string().nullable(),
68162
68611
  /** Ms epoch when the slice was last updated. */
@@ -68234,7 +68683,7 @@ var ValveStatusSchema = object({
68234
68683
  ]),
68235
68684
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68236
68685
  * intermediate position surface. */
68237
- position: number().min(0).max(100).nullable(),
68686
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68238
68687
  /** Ms epoch when the slice was last updated. */
68239
68688
  lastChangedAt: number()
68240
68689
  });
@@ -68342,9 +68791,9 @@ var vibrationCapability = {
68342
68791
  */
68343
68792
  var WaterHeaterStatusSchema = object({
68344
68793
  /** Current measured temperature. Null when not reported. */
68345
- currentTemp: number().nullable(),
68794
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68346
68795
  /** Target temperature setpoint. Null when no setpoint surface. */
68347
- targetTemp: number().nullable(),
68796
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68348
68797
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68349
68798
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68350
68799
  * device reports an unknown state. */
@@ -68355,9 +68804,9 @@ var WaterHeaterStatusSchema = object({
68355
68804
  * has no away surface. */
68356
68805
  away: boolean().nullable(),
68357
68806
  /** HA `min_temp` attribute. Null when not reported. */
68358
- minTemp: number().nullable(),
68807
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68359
68808
  /** HA `max_temp` attribute. Null when not reported. */
68360
- maxTemp: number().nullable(),
68809
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68361
68810
  /** Ms epoch when the slice was last updated. */
68362
68811
  lastChangedAt: number()
68363
68812
  });
@@ -68442,25 +68891,18 @@ var waterHeaterCapability = {
68442
68891
  * Forecast deferred — current conditions only for v1.
68443
68892
  */
68444
68893
  var WeatherStatusSchema = object({
68445
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68446
- * when no condition has been reported yet. */
68894
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68447
68895
  condition: string().nullable(),
68448
- /** Current temperature in the reported unit. Null when not provided. */
68449
- temperature: number().nullable(),
68450
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68451
- temperatureUnit: string().nullable(),
68896
+ /** Current temperature in Celsius. Null when not provided or refused. */
68897
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68452
68898
  /** Relative humidity (0..100). Null when not provided. */
68453
- humidity: number().min(0).max(100).nullable(),
68454
- /** Barometric pressure in the reported unit. Null when not provided. */
68455
- pressure: number().nullable(),
68456
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68457
- pressureUnit: string().nullable(),
68458
- /** Wind speed in the reported unit. Null when not provided. */
68459
- windSpeed: number().nullable(),
68460
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68461
- windSpeedUnit: string().nullable(),
68899
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68900
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68901
+ pressure: number().nullable().meta(readingMeta("pressure")),
68902
+ /** Wind speed in metres per second. Null when not provided or refused. */
68903
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68462
68904
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68463
- windBearing: number().nullable(),
68905
+ windBearing: number().nullable().meta(readingMeta("angle")),
68464
68906
  /** Ms epoch when the slice was last updated. */
68465
68907
  lastFetchedAt: number()
68466
68908
  });
@@ -68488,12 +68930,12 @@ var weatherCapability = {
68488
68930
  */
68489
68931
  durability: "session",
68490
68932
  /**
68491
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68933
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68492
68934
  */
68493
68935
  history: { fields: {
68494
68936
  temperature: {
68495
68937
  kind: "gauge",
68496
- unitFrom: "temperatureUnit",
68938
+ unit: "°C",
68497
68939
  deadband: .2
68498
68940
  },
68499
68941
  humidity: {
@@ -68503,12 +68945,12 @@ var weatherCapability = {
68503
68945
  },
68504
68946
  pressure: {
68505
68947
  kind: "gauge",
68506
- unitFrom: "pressureUnit",
68948
+ unit: "hPa",
68507
68949
  deadband: .5
68508
68950
  },
68509
68951
  windSpeed: {
68510
68952
  kind: "gauge",
68511
- unitFrom: "windSpeedUnit",
68953
+ unit: "m/s",
68512
68954
  deadband: .5
68513
68955
  }
68514
68956
  } }
@@ -69409,6 +69851,389 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69409
69851
  kind: "mutation",
69410
69852
  auth: "admin"
69411
69853
  });
69854
+ /**
69855
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
69856
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
69857
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
69858
+ * decimals). K derives from `°C = K − 273.15`.
69859
+ */
69860
+ var UNIT_TABLE = {
69861
+ "°C": {
69862
+ dimension: "temperature",
69863
+ factor: 1,
69864
+ offset: 0
69865
+ },
69866
+ "°F": {
69867
+ dimension: "temperature",
69868
+ factor: 5 / 9,
69869
+ offset: -160 / 9
69870
+ },
69871
+ K: {
69872
+ dimension: "temperature",
69873
+ factor: 1,
69874
+ offset: -273.15
69875
+ },
69876
+ hPa: {
69877
+ dimension: "pressure",
69878
+ factor: 1,
69879
+ offset: 0
69880
+ },
69881
+ kPa: {
69882
+ dimension: "pressure",
69883
+ factor: 10,
69884
+ offset: 0
69885
+ },
69886
+ Pa: {
69887
+ dimension: "pressure",
69888
+ factor: .01,
69889
+ offset: 0
69890
+ },
69891
+ mbar: {
69892
+ dimension: "pressure",
69893
+ factor: 1,
69894
+ offset: 0
69895
+ },
69896
+ bar: {
69897
+ dimension: "pressure",
69898
+ factor: 1e3,
69899
+ offset: 0
69900
+ },
69901
+ inHg: {
69902
+ dimension: "pressure",
69903
+ factor: 33.8639,
69904
+ offset: 0
69905
+ },
69906
+ mmHg: {
69907
+ dimension: "pressure",
69908
+ factor: 1.33322,
69909
+ offset: 0
69910
+ },
69911
+ psi: {
69912
+ dimension: "pressure",
69913
+ factor: 68.9476,
69914
+ offset: 0
69915
+ },
69916
+ "m/s": {
69917
+ dimension: "speed",
69918
+ factor: 1,
69919
+ offset: 0
69920
+ },
69921
+ "km/h": {
69922
+ dimension: "speed",
69923
+ factor: 1 / 3.6,
69924
+ offset: 0
69925
+ },
69926
+ mph: {
69927
+ dimension: "speed",
69928
+ factor: .44704,
69929
+ offset: 0
69930
+ },
69931
+ kn: {
69932
+ dimension: "speed",
69933
+ factor: .514444,
69934
+ offset: 0
69935
+ },
69936
+ "mm/h": {
69937
+ dimension: "precipitation-rate",
69938
+ factor: 1,
69939
+ offset: 0
69940
+ },
69941
+ "in/h": {
69942
+ dimension: "precipitation-rate",
69943
+ factor: 25.4,
69944
+ offset: 0
69945
+ },
69946
+ m: {
69947
+ dimension: "length",
69948
+ factor: 1,
69949
+ offset: 0
69950
+ },
69951
+ mm: {
69952
+ dimension: "length",
69953
+ factor: .001,
69954
+ offset: 0
69955
+ },
69956
+ cm: {
69957
+ dimension: "length",
69958
+ factor: .01,
69959
+ offset: 0
69960
+ },
69961
+ km: {
69962
+ dimension: "length",
69963
+ factor: 1e3,
69964
+ offset: 0
69965
+ },
69966
+ in: {
69967
+ dimension: "length",
69968
+ factor: .0254,
69969
+ offset: 0
69970
+ },
69971
+ ft: {
69972
+ dimension: "length",
69973
+ factor: .3048,
69974
+ offset: 0
69975
+ },
69976
+ mi: {
69977
+ dimension: "length",
69978
+ factor: 1609.344,
69979
+ offset: 0
69980
+ },
69981
+ "m²": {
69982
+ dimension: "area",
69983
+ factor: 1,
69984
+ offset: 0
69985
+ },
69986
+ lx: {
69987
+ dimension: "illuminance",
69988
+ factor: 1,
69989
+ offset: 0
69990
+ },
69991
+ "W/m²": {
69992
+ dimension: "irradiance",
69993
+ factor: 1,
69994
+ offset: 0
69995
+ },
69996
+ W: {
69997
+ dimension: "power",
69998
+ factor: 1,
69999
+ offset: 0
70000
+ },
70001
+ kW: {
70002
+ dimension: "power",
70003
+ factor: 1e3,
70004
+ offset: 0
70005
+ },
70006
+ VA: {
70007
+ dimension: "apparent-power",
70008
+ factor: 1,
70009
+ offset: 0
70010
+ },
70011
+ Wh: {
70012
+ dimension: "energy",
70013
+ factor: 1,
70014
+ offset: 0
70015
+ },
70016
+ kWh: {
70017
+ dimension: "energy",
70018
+ factor: 1e3,
70019
+ offset: 0
70020
+ },
70021
+ MWh: {
70022
+ dimension: "energy",
70023
+ factor: 1e6,
70024
+ offset: 0
70025
+ },
70026
+ V: {
70027
+ dimension: "voltage",
70028
+ factor: 1,
70029
+ offset: 0
70030
+ },
70031
+ mV: {
70032
+ dimension: "voltage",
70033
+ factor: .001,
70034
+ offset: 0
70035
+ },
70036
+ A: {
70037
+ dimension: "current",
70038
+ factor: 1,
70039
+ offset: 0
70040
+ },
70041
+ mA: {
70042
+ dimension: "current",
70043
+ factor: .001,
70044
+ offset: 0
70045
+ },
70046
+ ppm: {
70047
+ dimension: "concentration-volume",
70048
+ factor: 1,
70049
+ offset: 0
70050
+ },
70051
+ ppb: {
70052
+ dimension: "concentration-volume",
70053
+ factor: .001,
70054
+ offset: 0
70055
+ },
70056
+ "µg/m³": {
70057
+ dimension: "concentration-mass",
70058
+ factor: 1,
70059
+ offset: 0
70060
+ },
70061
+ "mg/m³": {
70062
+ dimension: "concentration-mass",
70063
+ factor: 1e3,
70064
+ offset: 0
70065
+ },
70066
+ kg: {
70067
+ dimension: "mass",
70068
+ factor: 1,
70069
+ offset: 0
70070
+ },
70071
+ g: {
70072
+ dimension: "mass",
70073
+ factor: .001,
70074
+ offset: 0
70075
+ },
70076
+ lb: {
70077
+ dimension: "mass",
70078
+ factor: .453592,
70079
+ offset: 0
70080
+ },
70081
+ oz: {
70082
+ dimension: "mass",
70083
+ factor: .0283495,
70084
+ offset: 0
70085
+ },
70086
+ "%": {
70087
+ dimension: "percentage",
70088
+ factor: 1,
70089
+ offset: 0
70090
+ },
70091
+ L: {
70092
+ dimension: "volume",
70093
+ factor: 1,
70094
+ offset: 0
70095
+ },
70096
+ mL: {
70097
+ dimension: "volume",
70098
+ factor: .001,
70099
+ offset: 0
70100
+ },
70101
+ gal: {
70102
+ dimension: "volume",
70103
+ factor: 3.785411784,
70104
+ offset: 0
70105
+ },
70106
+ s: {
70107
+ dimension: "duration",
70108
+ factor: 1,
70109
+ offset: 0
70110
+ },
70111
+ ms: {
70112
+ dimension: "duration",
70113
+ factor: .001,
70114
+ offset: 0
70115
+ },
70116
+ min: {
70117
+ dimension: "duration",
70118
+ factor: 60,
70119
+ offset: 0
70120
+ },
70121
+ h: {
70122
+ dimension: "duration",
70123
+ factor: 3600,
70124
+ offset: 0
70125
+ },
70126
+ d: {
70127
+ dimension: "duration",
70128
+ factor: 86400,
70129
+ offset: 0
70130
+ },
70131
+ B: {
70132
+ dimension: "information",
70133
+ factor: 1,
70134
+ offset: 0
70135
+ },
70136
+ KiB: {
70137
+ dimension: "information",
70138
+ factor: 1024,
70139
+ offset: 0
70140
+ },
70141
+ MiB: {
70142
+ dimension: "information",
70143
+ factor: 1024 ** 2,
70144
+ offset: 0
70145
+ },
70146
+ GiB: {
70147
+ dimension: "information",
70148
+ factor: 1024 ** 3,
70149
+ offset: 0
70150
+ },
70151
+ TiB: {
70152
+ dimension: "information",
70153
+ factor: 1024 ** 4,
70154
+ offset: 0
70155
+ },
70156
+ kB: {
70157
+ dimension: "information",
70158
+ factor: 1e3,
70159
+ offset: 0
70160
+ },
70161
+ MB: {
70162
+ dimension: "information",
70163
+ factor: 1e6,
70164
+ offset: 0
70165
+ },
70166
+ GB: {
70167
+ dimension: "information",
70168
+ factor: 1e9,
70169
+ offset: 0
70170
+ },
70171
+ "°": {
70172
+ dimension: "angle",
70173
+ factor: 1,
70174
+ offset: 0
70175
+ },
70176
+ dBFS: {
70177
+ dimension: "sound-level",
70178
+ factor: 1,
70179
+ offset: 0
70180
+ },
70181
+ dBm: {
70182
+ dimension: "signal-level",
70183
+ factor: 1,
70184
+ offset: 0
70185
+ },
70186
+ mired: {
70187
+ dimension: "colour-temperature",
70188
+ factor: 1,
70189
+ offset: 0
70190
+ },
70191
+ kbps: {
70192
+ dimension: "bitrate",
70193
+ factor: 1,
70194
+ offset: 0
70195
+ },
70196
+ Mbps: {
70197
+ dimension: "bitrate",
70198
+ factor: 1e3,
70199
+ offset: 0
70200
+ },
70201
+ fps: {
70202
+ dimension: "rate",
70203
+ factor: 1,
70204
+ offset: 0
70205
+ }
70206
+ };
70207
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
70208
+ function specFor(unit) {
70209
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
70210
+ }
70211
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
70212
+ function unitDimension(unit) {
70213
+ return specFor(unit)?.dimension;
70214
+ }
70215
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
70216
+ function toCanonical(value, spec) {
70217
+ return value * spec.factor + spec.offset;
70218
+ }
70219
+ /** Canonical-unit value → the unit described by `spec`. */
70220
+ function fromCanonical(canonical, spec) {
70221
+ return (canonical - spec.offset) / spec.factor;
70222
+ }
70223
+ /**
70224
+ * Graceful variant for the render path: returns the converted value, or
70225
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
70226
+ * Never throws — a render caller falls back to the raw value + source unit.
70227
+ */
70228
+ function tryConvertUnit(value, from, to) {
70229
+ if (from === to) return value;
70230
+ const fromSpec = specFor(from);
70231
+ if (!fromSpec) return void 0;
70232
+ const toSpec = specFor(to);
70233
+ if (!toSpec) return void 0;
70234
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
70235
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
70236
+ }
69412
70237
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69413
70238
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Camera, DeviceType.Camera, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
69414
70239
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -69739,6 +70564,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69739
70564
  featureProbe: featureProbeCapability,
69740
70565
  flood: floodCapability,
69741
70566
  gas: gasCapability,
70567
+ groupCommands: groupCommandsCapability,
70568
+ groupStatus: groupStatusCapability,
69742
70569
  heatingValve: heatingValveCapability,
69743
70570
  humidifier: humidifierCapability,
69744
70571
  humiditySensor: humiditySensorCapability,
@@ -71115,6 +71942,13 @@ Object.freeze({
71115
71942
  access: "view",
71116
71943
  role: "admin"
71117
71944
  },
71945
+ "addons.getPackageDocs": {
71946
+ capName: "addons",
71947
+ capScope: "system",
71948
+ addonId: null,
71949
+ access: "view",
71950
+ role: "admin"
71951
+ },
71118
71952
  "addons.getVersions": {
71119
71953
  capName: "addons",
71120
71954
  capScope: "system",
@@ -74223,6 +75057,20 @@ Object.freeze({
74223
75057
  access: "view",
74224
75058
  role: "viewer"
74225
75059
  },
75060
+ "groupCommands.getStatus": {
75061
+ capName: "group-commands",
75062
+ capScope: "device",
75063
+ addonId: null,
75064
+ access: "view",
75065
+ role: "viewer"
75066
+ },
75067
+ "groupStatus.getStatus": {
75068
+ capName: "group-status",
75069
+ capScope: "device",
75070
+ addonId: null,
75071
+ access: "view",
75072
+ role: "viewer"
75073
+ },
74226
75074
  "heatingValve.getStatus": {
74227
75075
  capName: "heating-valve",
74228
75076
  capScope: "device",
@@ -84045,6 +84893,37 @@ var SENSOR_CAP_VOCABULARY = {
84045
84893
  icon: "flame"
84046
84894
  }]
84047
84895
  },
84896
+ "group-commands": {
84897
+ scope: "device",
84898
+ noun: t("Group command", "Comando di gruppo")
84899
+ },
84900
+ "group-status": {
84901
+ scope: "device",
84902
+ noun: t("Group", "Gruppo"),
84903
+ state: [
84904
+ {
84905
+ kind: "numeric",
84906
+ field: "included",
84907
+ quantity: t("the members reporting", "i membri che rispondono"),
84908
+ label: t("Reporting", "Rispondono"),
84909
+ icon: "users"
84910
+ },
84911
+ {
84912
+ kind: "numeric",
84913
+ field: "excluded",
84914
+ quantity: t("the members left out", "i membri esclusi"),
84915
+ label: t("Excluded", "Esclusi"),
84916
+ icon: "users"
84917
+ },
84918
+ {
84919
+ kind: "numeric",
84920
+ field: "total",
84921
+ quantity: t("the member count", "il numero di membri"),
84922
+ label: t("Members", "Membri"),
84923
+ icon: "sigma"
84924
+ }
84925
+ ]
84926
+ },
84048
84927
  "humidity-sensor": {
84049
84928
  scope: "device",
84050
84929
  noun: t("Humidity", "Umidità"),
@@ -84078,7 +84957,6 @@ var SENSOR_CAP_VOCABULARY = {
84078
84957
  field: "value",
84079
84958
  quantity: t("the reading", "il valore"),
84080
84959
  label: t("Reading", "Valore"),
84081
- unitField: "unit",
84082
84960
  icon: "gauge"
84083
84961
  }]
84084
84962
  },
@@ -84214,7 +85092,7 @@ var SENSOR_CAP_VOCABULARY = {
84214
85092
  field: "temperature",
84215
85093
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84216
85094
  label: t("Temperature", "Temperatura"),
84217
- unitField: "temperatureUnit",
85095
+ unit: "°C",
84218
85096
  icon: "thermometer"
84219
85097
  },
84220
85098
  {
@@ -84230,7 +85108,7 @@ var SENSOR_CAP_VOCABULARY = {
84230
85108
  field: "windSpeed",
84231
85109
  quantity: t("the wind speed", "la velocità del vento"),
84232
85110
  label: t("Wind", "Vento"),
84233
- unitField: "windSpeedUnit",
85111
+ unit: "m/s",
84234
85112
  icon: "wind"
84235
85113
  },
84236
85114
  {
@@ -84238,7 +85116,7 @@ var SENSOR_CAP_VOCABULARY = {
84238
85116
  field: "pressure",
84239
85117
  quantity: t("the air pressure", "la pressione atmosferica"),
84240
85118
  label: t("Pressure", "Pressione"),
84241
- unitField: "pressureUnit",
85119
+ unit: "hPa",
84242
85120
  icon: "gauge"
84243
85121
  }
84244
85122
  ]
@@ -85511,6 +86389,16 @@ Object.freeze({
85511
86389
  form: "single",
85512
86390
  optional: false
85513
86391
  }],
86392
+ "groupCommands.getStatus": [{
86393
+ name: "deviceId",
86394
+ form: "single",
86395
+ optional: false
86396
+ }],
86397
+ "groupStatus.getStatus": [{
86398
+ name: "deviceId",
86399
+ form: "single",
86400
+ optional: false
86401
+ }],
85514
86402
  "heatingValve.getStatus": [{
85515
86403
  name: "deviceId",
85516
86404
  form: "single",
@@ -87495,6 +88383,8 @@ new Map(Object.entries({
87495
88383
  "detection-blind": "device-events",
87496
88384
  "motion-noisy": "device-events",
87497
88385
  "motion-calm": "device-events",
88386
+ "package-llm-silent": "device-events",
88387
+ "package-llm-answered": "device-events",
87498
88388
  "recording-stalled": "device-events",
87499
88389
  "recording-healthy": "device-events",
87500
88390
  "recording-degraded": "device-events",
@@ -87578,6 +88468,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87578
88468
  label: "Motion without a subject",
87579
88469
  kinds: ["motion-noisy", "motion-calm"]
87580
88470
  },
88471
+ {
88472
+ id: "package-llm",
88473
+ family: "device-events",
88474
+ label: "Package confirm",
88475
+ kinds: ["package-llm-silent", "package-llm-answered"]
88476
+ },
87581
88477
  {
87582
88478
  id: "recording",
87583
88479
  family: "device-events",
@@ -87759,6 +88655,8 @@ var DEVICE_EVENTS = [
87759
88655
  "detection-blind",
87760
88656
  "motion-noisy",
87761
88657
  "motion-calm",
88658
+ "package-llm-silent",
88659
+ "package-llm-answered",
87762
88660
  "recording-stalled",
87763
88661
  "recording-healthy",
87764
88662
  "recording-degraded",
@@ -88027,6 +88925,15 @@ new Map([
88027
88925
  snapshotLabel: "Cameras with constant motion now",
88028
88926
  snapshotSection: null
88029
88927
  },
88928
+ {
88929
+ id: "package-llm-confirm",
88930
+ family: "device-events",
88931
+ label: "Package confirm (not answering / answering again)",
88932
+ bad: ["package-llm-silent"],
88933
+ recovery: ["package-llm-answered"],
88934
+ snapshotLabel: "Cameras whose package confirm is not answering",
88935
+ snapshotSection: null
88936
+ },
88030
88937
  {
88031
88938
  id: "recording-health",
88032
88939
  family: "device-events",
@@ -88604,6 +89511,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
88604
89511
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
88605
89512
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
88606
89513
  /**
89514
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
89515
+ * An unknown unit or a unit from another dimension is a refusal: the caller
89516
+ * logs it and publishes null (D393). This function never guesses.
89517
+ */
89518
+ var DIMENSION_QUANTITY = {
89519
+ temperature: "temperature",
89520
+ pressure: "pressure",
89521
+ speed: "speed",
89522
+ "precipitation-rate": "precipitation-rate",
89523
+ length: "length",
89524
+ area: "area",
89525
+ illuminance: "illuminance",
89526
+ irradiance: "irradiance",
89527
+ power: "power",
89528
+ "apparent-power": "apparent-power",
89529
+ energy: "energy",
89530
+ voltage: "voltage",
89531
+ current: "current",
89532
+ "concentration-volume": "concentration-volume",
89533
+ "concentration-mass": "concentration-mass",
89534
+ mass: "mass",
89535
+ percentage: "percentage",
89536
+ volume: "volume",
89537
+ duration: "duration",
89538
+ information: "information",
89539
+ angle: "angle",
89540
+ "sound-level": "sound-level",
89541
+ "signal-level": "signal-level",
89542
+ "colour-temperature": "colour-temperature",
89543
+ bitrate: "bitrate",
89544
+ rate: "rate"
89545
+ };
89546
+ /**
89547
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
89548
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
89549
+ * precipitation is chosen by the caller (a device class), not by the symbol.
89550
+ */
89551
+ function quantityFromUnit(rawUnit) {
89552
+ const unit = normalizeUnit(rawUnit);
89553
+ if (unit === void 0) return void 0;
89554
+ if (unit === "ppb") return "concentration-ppb";
89555
+ if (unit === "ms") return "duration-ms";
89556
+ const dimension = unitDimension(unit);
89557
+ if (dimension === void 0) return void 0;
89558
+ return DIMENSION_QUANTITY[dimension];
89559
+ }
89560
+ function ingestReading(input) {
89561
+ if (!Number.isFinite(input.value)) return {
89562
+ ok: false,
89563
+ reason: "non-finite"
89564
+ };
89565
+ if (input.quantity === "device") return {
89566
+ ok: false,
89567
+ reason: "unknown-unit"
89568
+ };
89569
+ const raw = input.rawUnit?.trim();
89570
+ if (raw === void 0 || raw.length === 0) return {
89571
+ ok: true,
89572
+ value: input.value
89573
+ };
89574
+ const unit = normalizeUnit(raw);
89575
+ if (unit === void 0) return {
89576
+ ok: false,
89577
+ reason: "unknown-unit"
89578
+ };
89579
+ const base = baseUnitOf(input.quantity);
89580
+ if (base === void 0) {
89581
+ if (unitDimension(unit) !== void 0) return {
89582
+ ok: false,
89583
+ reason: "cross-dimension"
89584
+ };
89585
+ return {
89586
+ ok: false,
89587
+ reason: "unknown-unit"
89588
+ };
89589
+ }
89590
+ const converted = tryConvertUnit(input.value, unit, base);
89591
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
89592
+ ok: false,
89593
+ reason: "unknown-unit"
89594
+ } : {
89595
+ ok: false,
89596
+ reason: "cross-dimension"
89597
+ };
89598
+ return {
89599
+ ok: true,
89600
+ value: converted
89601
+ };
89602
+ }
89603
+ /**
88607
89604
  * Pure fuzzy matcher for adoption location import. Normalized
88608
89605
  * case-insensitive Levenshtein similarity, used to reuse an existing
88609
89606
  * CamStack location when an imported source location (e.g. an HA area) is
@@ -114238,11 +115235,7 @@ var hmDiagnosticSchema = hmChildSchema.extend({
114238
115235
  sourceParams: array(string()).min(1)
114239
115236
  });
114240
115237
  /** Cold-start slices keyed by cap. */
114241
- var COLD_NUMERIC = (unit) => unit !== void 0 ? {
114242
- value: 0,
114243
- unit,
114244
- lastFetchedAt: 0
114245
- } : {
115238
+ var COLD_NUMERIC = {
114246
115239
  value: 0,
114247
115240
  lastFetchedAt: 0
114248
115241
  };
@@ -114286,7 +115279,7 @@ var HmDiagnosticDevice = class extends HmChildDevice {
114286
115279
  this.runtimeState.setCapState("temperature-sensor", COLD_TEMP);
114287
115280
  return;
114288
115281
  default: {
114289
- const cold = COLD_NUMERIC(this.unit);
115282
+ const cold = COLD_NUMERIC;
114290
115283
  this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState("numeric-sensor") ?? cold });
114291
115284
  this.runtimeState.setCapState("numeric-sensor", cold);
114292
115285
  return;
@@ -114311,22 +115304,55 @@ var HmDiagnosticDevice = class extends HmChildDevice {
114311
115304
  return;
114312
115305
  }
114313
115306
  if (typeof value !== "number") return;
115307
+ const stored = this.toBase(value);
115308
+ if (stored === null) return;
114314
115309
  if (this.capName === "temperature-sensor") {
114315
115310
  this.runtimeState.setCapState("temperature-sensor", {
114316
- celsius: value,
115311
+ celsius: stored,
114317
115312
  lastFetchedAt: now
114318
115313
  });
114319
115314
  return;
114320
115315
  }
114321
- const next = this.unit !== void 0 ? {
114322
- value,
114323
- unit: this.unit,
115316
+ this.runtimeState.setCapState("numeric-sensor", {
115317
+ value: stored,
114324
115318
  lastFetchedAt: now
114325
- } : {
115319
+ });
115320
+ }
115321
+ /** Upstream diagnostic number → the quantity's base. Unknown units publish nothing. */
115322
+ toBase(value) {
115323
+ const quantity = this.readingQuantity();
115324
+ if (quantity === null) {
115325
+ this.warnRefusal("unknown-unit");
115326
+ return null;
115327
+ }
115328
+ const result = ingestReading({
114326
115329
  value,
114327
- lastFetchedAt: now
114328
- };
114329
- this.runtimeState.setCapState("numeric-sensor", next);
115330
+ rawUnit: this.unit,
115331
+ quantity
115332
+ });
115333
+ if (!result.ok) {
115334
+ this.warnRefusal(result.reason);
115335
+ return null;
115336
+ }
115337
+ if (this.capName === "numeric-sensor" && this.sourceInfo.quantity !== quantity) this.updateSourceInfo({ quantity });
115338
+ return result.value;
115339
+ }
115340
+ readingQuantity() {
115341
+ if (this.capName === "temperature-sensor") return "temperature";
115342
+ if (this.unit === void 0) return "none";
115343
+ const learned = quantityFromUnit(this.unit);
115344
+ if (learned === void 0 || learned === "device") return null;
115345
+ return learned;
115346
+ }
115347
+ warnRefusal(reason) {
115348
+ this.ctx.logger.warn("numeric reading refused", {
115349
+ tags: { deviceId: this.id },
115350
+ meta: {
115351
+ quantity: this.readingQuantity() ?? "device",
115352
+ unit: this.unit ?? "",
115353
+ reason
115354
+ }
115355
+ });
114330
115356
  }
114331
115357
  };
114332
115358
  //#endregion