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