@camstack/addon-terminal 0.1.152 → 0.1.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -31,7 +31,7 @@ let node_module = require("node:module");
31
31
  let sharp = require("sharp");
32
32
  sharp = __toESM(sharp);
33
33
  let node_http = require("node:http");
34
- //#region ../types/dist/event-category-DHXxB8t9.mjs
34
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
35
35
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
36
  EventCategory["SystemBoot"] = "system.boot";
37
37
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -357,6 +357,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
357
357
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
358
358
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
359
359
  /**
360
+ * A camera's package LLM confirm has failed closed three times in a row
361
+ * (D931, 2026-10-26). One edge per camera until
362
+ * {@link PipelinePackageLlmAnswered}.
363
+ */
364
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
365
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
366
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
367
+ /**
360
368
  * Per-camera pipeline config was mutated by the orchestrator
361
369
  * (a settings change via `setAgentInferenceDevices` /
362
370
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5466,7 +5474,7 @@ var ZodIssueCode = {
5466
5474
  var ZodFirstPartyTypeKind;
5467
5475
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5468
5476
  //#endregion
5469
- //#region ../types/dist/sleep-BErAxhul.mjs
5477
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5470
5478
  /**
5471
5479
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5472
5480
  * window to float samples (D455).
@@ -8020,6 +8028,99 @@ var RecordKindDeclarationSchema = object({
8020
8028
  }).strict();
8021
8029
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8022
8030
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8031
+ var READING_QUANTITIES = [
8032
+ "temperature",
8033
+ "pressure",
8034
+ "speed",
8035
+ "precipitation",
8036
+ "precipitation-rate",
8037
+ "length",
8038
+ "area",
8039
+ "illuminance",
8040
+ "irradiance",
8041
+ "power",
8042
+ "energy",
8043
+ "voltage",
8044
+ "current",
8045
+ "apparent-power",
8046
+ "humidity",
8047
+ "percentage",
8048
+ "concentration-volume",
8049
+ "concentration-mass",
8050
+ "concentration-ppb",
8051
+ "volume",
8052
+ "duration",
8053
+ "duration-ms",
8054
+ "information",
8055
+ "sound-level",
8056
+ "signal-level",
8057
+ "colour-temperature",
8058
+ "angle",
8059
+ "bitrate",
8060
+ "rate",
8061
+ "mass",
8062
+ "index",
8063
+ "ratio",
8064
+ "count",
8065
+ "none",
8066
+ "device"
8067
+ ];
8068
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8069
+ new Set(READING_QUANTITIES);
8070
+ /**
8071
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8072
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8073
+ */
8074
+ var QUANTITY_BASE = {
8075
+ temperature: "°C",
8076
+ pressure: "hPa",
8077
+ speed: "m/s",
8078
+ precipitation: "mm",
8079
+ "precipitation-rate": "mm/h",
8080
+ length: "m",
8081
+ area: "m²",
8082
+ illuminance: "lx",
8083
+ irradiance: "W/m²",
8084
+ power: "W",
8085
+ energy: "kWh",
8086
+ voltage: "V",
8087
+ current: "A",
8088
+ "apparent-power": "VA",
8089
+ humidity: "%",
8090
+ percentage: "%",
8091
+ "concentration-volume": "ppm",
8092
+ "concentration-mass": "µg/m³",
8093
+ "concentration-ppb": "ppb",
8094
+ volume: "L",
8095
+ duration: "s",
8096
+ "duration-ms": "ms",
8097
+ information: "B",
8098
+ "sound-level": "dBFS",
8099
+ "signal-level": "dBm",
8100
+ "colour-temperature": "mired",
8101
+ angle: "°",
8102
+ bitrate: "kbps",
8103
+ rate: "fps",
8104
+ mass: "kg",
8105
+ index: "none",
8106
+ ratio: "none",
8107
+ count: "none",
8108
+ none: "none",
8109
+ device: "device"
8110
+ };
8111
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8112
+ function baseUnitOf(quantity) {
8113
+ const symbol = QUANTITY_BASE[quantity];
8114
+ if (symbol === "none" || symbol === "device") return void 0;
8115
+ return symbol;
8116
+ }
8117
+ function readingMeta(quantity, extras) {
8118
+ return {
8119
+ quantity,
8120
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8121
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8122
+ };
8123
+ }
8023
8124
  /** Shared Zod schemas used across streaming capabilities. */
8024
8125
  var CamProfileSchema = _enum([
8025
8126
  "high",
@@ -8047,8 +8148,8 @@ var CamStreamKindSchema = _enum([
8047
8148
  "derived"
8048
8149
  ]);
8049
8150
  var CamStreamResolutionSchema = object({
8050
- width: number().int().positive(),
8051
- height: number().int().positive()
8151
+ width: number().int().positive().meta(readingMeta("none")),
8152
+ height: number().int().positive().meta(readingMeta("none"))
8052
8153
  });
8053
8154
  var CameraStreamSchema = object({
8054
8155
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8625,6 +8726,8 @@ var CAP_NAMES = Object.freeze([
8625
8726
  "filesystem-browse",
8626
8727
  "flood",
8627
8728
  "gas",
8729
+ "group-commands",
8730
+ "group-status",
8628
8731
  "heating-valve",
8629
8732
  "humidifier",
8630
8733
  "humidity-sensor",
@@ -8754,14 +8857,6 @@ Object.freeze({
8754
8857
  {
8755
8858
  "path": "aqi",
8756
8859
  "valueType": "number"
8757
- },
8758
- {
8759
- "path": "unit",
8760
- "valueType": "string"
8761
- },
8762
- {
8763
- "path": "precision",
8764
- "valueType": "number"
8765
8860
  }
8766
8861
  ],
8767
8862
  "alarm-panel": [
@@ -8797,20 +8892,10 @@ Object.freeze({
8797
8892
  "valueType": "boolean"
8798
8893
  }
8799
8894
  ],
8800
- "ambient-light-sensor": [
8801
- {
8802
- "path": "lux",
8803
- "valueType": "number"
8804
- },
8805
- {
8806
- "path": "unit",
8807
- "valueType": "string"
8808
- },
8809
- {
8810
- "path": "precision",
8811
- "valueType": "number"
8812
- }
8813
- ],
8895
+ "ambient-light-sensor": [{
8896
+ "path": "lux",
8897
+ "valueType": "number"
8898
+ }],
8814
8899
  "audio-metrics": [
8815
8900
  {
8816
8901
  "path": "ts",
@@ -8890,6 +8975,10 @@ Object.freeze({
8890
8975
  "path": "lastUpdated",
8891
8976
  "valueType": "number"
8892
8977
  },
8978
+ {
8979
+ "path": "volts",
8980
+ "valueType": "number"
8981
+ },
8893
8982
  {
8894
8983
  "path": "lastContactAt",
8895
8984
  "valueType": "number"
@@ -9132,10 +9221,6 @@ Object.freeze({
9132
9221
  "datetime"
9133
9222
  ]
9134
9223
  },
9135
- {
9136
- "path": "unit",
9137
- "valueType": "string"
9138
- },
9139
9224
  {
9140
9225
  "path": "min",
9141
9226
  "valueType": "number"
@@ -9147,19 +9232,6 @@ Object.freeze({
9147
9232
  {
9148
9233
  "path": "step",
9149
9234
  "valueType": "number"
9150
- },
9151
- {
9152
- "path": "precision",
9153
- "valueType": "number"
9154
- },
9155
- {
9156
- "path": "format",
9157
- "valueType": "enum",
9158
- "enumValues": [
9159
- "date",
9160
- "time",
9161
- "datetime"
9162
- ]
9163
9235
  }
9164
9236
  ],
9165
9237
  "cover": [
@@ -9400,13 +9472,8 @@ Object.freeze({
9400
9472
  "path": "value",
9401
9473
  "valueType": "string"
9402
9474
  }, {
9403
- "path": "format",
9404
- "valueType": "enum",
9405
- "enumValues": [
9406
- "date",
9407
- "time",
9408
- "datetime"
9409
- ]
9475
+ "path": "timestamp",
9476
+ "valueType": "boolean"
9410
9477
  }],
9411
9478
  "event-emitter": [
9412
9479
  {
@@ -9479,6 +9546,20 @@ Object.freeze({
9479
9546
  "path": "detected",
9480
9547
  "valueType": "boolean"
9481
9548
  }],
9549
+ "group-status": [
9550
+ {
9551
+ "path": "included",
9552
+ "valueType": "number"
9553
+ },
9554
+ {
9555
+ "path": "excluded",
9556
+ "valueType": "number"
9557
+ },
9558
+ {
9559
+ "path": "total",
9560
+ "valueType": "number"
9561
+ }
9562
+ ],
9482
9563
  "heating-valve": [{
9483
9564
  "path": "position",
9484
9565
  "valueType": "number"
@@ -9513,20 +9594,10 @@ Object.freeze({
9513
9594
  "valueType": "number"
9514
9595
  }
9515
9596
  ],
9516
- "humidity-sensor": [
9517
- {
9518
- "path": "percent",
9519
- "valueType": "number"
9520
- },
9521
- {
9522
- "path": "unit",
9523
- "valueType": "string"
9524
- },
9525
- {
9526
- "path": "precision",
9527
- "valueType": "number"
9528
- }
9529
- ],
9597
+ "humidity-sensor": [{
9598
+ "path": "percent",
9599
+ "valueType": "number"
9600
+ }],
9530
9601
  "image": [{
9531
9602
  "path": "url",
9532
9603
  "valueType": "string"
@@ -9865,20 +9936,10 @@ Object.freeze({
9865
9936
  "valueType": "boolean"
9866
9937
  }
9867
9938
  ],
9868
- "numeric-sensor": [
9869
- {
9870
- "path": "value",
9871
- "valueType": "number"
9872
- },
9873
- {
9874
- "path": "unit",
9875
- "valueType": "string"
9876
- },
9877
- {
9878
- "path": "precision",
9879
- "valueType": "number"
9880
- }
9881
- ],
9939
+ "numeric-sensor": [{
9940
+ "path": "value",
9941
+ "valueType": "number"
9942
+ }],
9882
9943
  "pet-feeder": [
9883
9944
  {
9884
9945
  "path": "foodLevel",
@@ -9970,14 +10031,6 @@ Object.freeze({
9970
10031
  {
9971
10032
  "path": "powerFactor",
9972
10033
  "valueType": "number"
9973
- },
9974
- {
9975
- "path": "unit",
9976
- "valueType": "string"
9977
- },
9978
- {
9979
- "path": "precision",
9980
- "valueType": "number"
9981
10034
  }
9982
10035
  ],
9983
10036
  "presence": [
@@ -10006,20 +10059,10 @@ Object.freeze({
10006
10059
  "valueType": "number"
10007
10060
  }
10008
10061
  ],
10009
- "pressure-sensor": [
10010
- {
10011
- "path": "hpa",
10012
- "valueType": "number"
10013
- },
10014
- {
10015
- "path": "unit",
10016
- "valueType": "string"
10017
- },
10018
- {
10019
- "path": "precision",
10020
- "valueType": "number"
10021
- }
10022
- ],
10062
+ "pressure-sensor": [{
10063
+ "path": "hpa",
10064
+ "valueType": "number"
10065
+ }],
10023
10066
  "privacy-mask": [{
10024
10067
  "path": "enabled",
10025
10068
  "valueType": "boolean"
@@ -10330,20 +10373,10 @@ Object.freeze({
10330
10373
  "path": "tampered",
10331
10374
  "valueType": "boolean"
10332
10375
  }],
10333
- "temperature-sensor": [
10334
- {
10335
- "path": "celsius",
10336
- "valueType": "number"
10337
- },
10338
- {
10339
- "path": "unit",
10340
- "valueType": "string"
10341
- },
10342
- {
10343
- "path": "precision",
10344
- "valueType": "number"
10345
- }
10346
- ],
10376
+ "temperature-sensor": [{
10377
+ "path": "celsius",
10378
+ "valueType": "number"
10379
+ }],
10347
10380
  "update": [
10348
10381
  {
10349
10382
  "path": "currentVersion",
@@ -10454,10 +10487,6 @@ Object.freeze({
10454
10487
  "path": "temperature",
10455
10488
  "valueType": "number"
10456
10489
  },
10457
- {
10458
- "path": "temperatureUnit",
10459
- "valueType": "string"
10460
- },
10461
10490
  {
10462
10491
  "path": "humidity",
10463
10492
  "valueType": "number"
@@ -10466,18 +10495,10 @@ Object.freeze({
10466
10495
  "path": "pressure",
10467
10496
  "valueType": "number"
10468
10497
  },
10469
- {
10470
- "path": "pressureUnit",
10471
- "valueType": "string"
10472
- },
10473
10498
  {
10474
10499
  "path": "windSpeed",
10475
10500
  "valueType": "number"
10476
10501
  },
10477
- {
10478
- "path": "windSpeedUnit",
10479
- "valueType": "string"
10480
- },
10481
10502
  {
10482
10503
  "path": "windBearing",
10483
10504
  "valueType": "number"
@@ -10880,7 +10901,7 @@ function errMsg(err) {
10880
10901
  return String(err);
10881
10902
  }
10882
10903
  //#endregion
10883
- //#region ../types/dist/composition-kEl9mevu.mjs
10904
+ //#region ../types/dist/composition-OhpuespA.mjs
10884
10905
  /**
10885
10906
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10886
10907
  *
@@ -11635,15 +11656,74 @@ function validateExpressionSource(src) {
11635
11656
  }
11636
11657
  return null;
11637
11658
  }
11638
- /** A composed device's stableId is this prefix plus the block id. */
11639
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11640
- /** Longest snippet a `code` source may carry (slice 4). */
11641
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11659
+ /**
11660
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11661
+ * id. Split out so helper schemas can name it without a cycle through
11662
+ * `composition.ts` (D659).
11663
+ */
11642
11664
  var CompositionSourceRefSchema = object({
11643
11665
  /** The owning addon; `stableId` is unique only within it. */
11644
11666
  addonId: string().min(1),
11645
11667
  stableId: string().min(1)
11646
11668
  });
11669
+ var GROUP_TEMPLATE_IDS = [
11670
+ "temperature",
11671
+ "humidity",
11672
+ "pressure",
11673
+ "illuminance",
11674
+ "power",
11675
+ "energy",
11676
+ "battery",
11677
+ "numeric",
11678
+ "motion",
11679
+ "contact",
11680
+ "leak",
11681
+ "smoke",
11682
+ "occupancy",
11683
+ "binary",
11684
+ "switch",
11685
+ "light"
11686
+ ];
11687
+ var NUMERIC_AGGREGATES = [
11688
+ "mean",
11689
+ "median",
11690
+ "min",
11691
+ "max",
11692
+ "sum"
11693
+ ];
11694
+ var BINARY_AGGREGATES = ["any", "all"];
11695
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11696
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11697
+ var AGGREGATE_REDUCERS = [
11698
+ "mean",
11699
+ "median",
11700
+ "min",
11701
+ "max",
11702
+ "sum",
11703
+ "any",
11704
+ "all"
11705
+ ];
11706
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11707
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11708
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11709
+ /** One member. The field is stored so occupancy can name motion or binary. */
11710
+ var HelperMemberSchema = object({
11711
+ source: CompositionSourceRefSchema,
11712
+ cap: string().min(1),
11713
+ fieldPath: string().min(1)
11714
+ });
11715
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11716
+ kind: literal("group"),
11717
+ templateId: GroupTemplateIdSchema,
11718
+ aggregate: AggregateReducerSchema,
11719
+ members: array(HelperMemberSchema).min(1).max(64),
11720
+ /** Actuator groups only. Readings leave this unset. */
11721
+ active: ActiveCriterionSchema.optional()
11722
+ })]);
11723
+ /** A composed device's stableId is this prefix plus the block id. */
11724
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11725
+ /** Longest snippet a `code` source may carry (slice 4). */
11726
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11647
11727
  /** One field of one capability of one source device. */
11648
11728
  var CompositionFieldReadSchema = object({
11649
11729
  source: CompositionSourceRefSchema,
@@ -11687,7 +11767,33 @@ var CompositionCodeSourceSchema = object({
11687
11767
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11688
11768
  CompositionFromSourceSchema,
11689
11769
  CompositionExpressionSourceSchema,
11690
- CompositionCodeSourceSchema
11770
+ CompositionCodeSourceSchema,
11771
+ object({
11772
+ kind: literal("aggregate"),
11773
+ reducer: _enum([
11774
+ "mean",
11775
+ "median",
11776
+ "min",
11777
+ "max",
11778
+ "sum",
11779
+ "any",
11780
+ "all"
11781
+ ]),
11782
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11783
+ /** `value` is the reading; the others are the member counts. */
11784
+ publish: _enum([
11785
+ "value",
11786
+ "included",
11787
+ "excluded",
11788
+ "total"
11789
+ ]),
11790
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11791
+ empty: _enum([
11792
+ "null",
11793
+ "omit",
11794
+ "offline"
11795
+ ])
11796
+ })
11691
11797
  ]);
11692
11798
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11693
11799
  kind: literal("forward"),
@@ -11733,7 +11839,13 @@ var CompositionSchema = object({
11733
11839
  kind: literal("existing"),
11734
11840
  device: CompositionSourceRefSchema
11735
11841
  })]),
11736
- features: array(CompositionFeatureSchema).min(1).max(16)
11842
+ features: array(CompositionFeatureSchema).min(1).max(16),
11843
+ /**
11844
+ * Present when the block is a helper template. Absent on a free
11845
+ * composition (the room example, device 4603). The editor reads this,
11846
+ * never the compiled features.
11847
+ */
11848
+ helper: HelperSpecSchema.optional()
11737
11849
  }).superRefine((composition, ctx) => {
11738
11850
  const seen = /* @__PURE__ */ new Set();
11739
11851
  composition.features.forEach((feature, index) => {
@@ -17973,7 +18085,11 @@ var CompositionProblemSchema = object({
17973
18085
  "item-field-reserved",
17974
18086
  "target-missing",
17975
18087
  "target-unreadable",
17976
- "not-replaceable"
18088
+ "not-replaceable",
18089
+ "native-capability-replace",
18090
+ "mixed-quantity",
18091
+ "helper-mismatch",
18092
+ "member-constraint"
17977
18093
  ]),
17978
18094
  path: string(),
17979
18095
  message: string()
@@ -17994,7 +18110,8 @@ var CompositionUnavailableModeSchema = _enum([
17994
18110
  "null",
17995
18111
  "offline",
17996
18112
  "unknown",
17997
- "release"
18113
+ "release",
18114
+ "omit"
17998
18115
  ]);
17999
18116
  /**
18000
18117
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18031,9 +18148,11 @@ var CompositionFieldPlanSchema = object({
18031
18148
  reads: array(CompositionFieldReadSchema)
18032
18149
  });
18033
18150
  /**
18034
- * What a feature does to its target (D663): `new` builds a capability on a new
18035
- * device; on an existing device it `add`s a capability the device lacks, or
18036
- * `replace`s named fields of one it has natively.
18151
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18152
+ * new device; on an existing device it `add`s a capability the device lacks.
18153
+ * Replacing a capability the device already serves is refused
18154
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18155
+ * wire for a claim the hub already holds.
18037
18156
  */
18038
18157
  var CompositionFeatureModeSchema = _enum([
18039
18158
  "new",
@@ -18206,6 +18325,275 @@ Object.freeze({
18206
18325
  result: ["number", "null"]
18207
18326
  }
18208
18327
  });
18328
+ /**
18329
+ * One catalog row per base reading a group helper can build (D944 §3).
18330
+ * Data, not a form default written into the block.
18331
+ */
18332
+ var NOW = {
18333
+ fieldPath: "lastFetchedAt",
18334
+ expr: "now"
18335
+ };
18336
+ var CHANGED = {
18337
+ fieldPath: "lastChangedAt",
18338
+ expr: "now"
18339
+ };
18340
+ function numeric(input) {
18341
+ return {
18342
+ ...input,
18343
+ family: "numeric",
18344
+ deviceType: DeviceType.Sensor,
18345
+ memberDeviceType: DeviceType.Sensor,
18346
+ commandCaps: [],
18347
+ baseUnit: baseUnitOf(input.quantity),
18348
+ aggregates: NUMERIC_AGGREGATES,
18349
+ accept: [{
18350
+ cap: input.outputCap,
18351
+ fieldPath: input.outputField
18352
+ }]
18353
+ };
18354
+ }
18355
+ function binary(input) {
18356
+ return {
18357
+ ...input,
18358
+ family: "binary",
18359
+ deviceType: DeviceType.Sensor,
18360
+ memberDeviceType: DeviceType.Sensor,
18361
+ commandCaps: [],
18362
+ quantity: "none",
18363
+ baseUnit: void 0,
18364
+ aggregates: BINARY_AGGREGATES,
18365
+ empty: "offline"
18366
+ };
18367
+ }
18368
+ var GROUP_TEMPLATES = [
18369
+ numeric({
18370
+ id: "temperature",
18371
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18372
+ role: DeviceRole.TemperatureSensor,
18373
+ outputCap: "temperature-sensor",
18374
+ outputField: "celsius",
18375
+ quantity: "temperature",
18376
+ defaultAggregate: "mean",
18377
+ empty: "null",
18378
+ companions: [NOW]
18379
+ }),
18380
+ numeric({
18381
+ id: "humidity",
18382
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18383
+ role: DeviceRole.HumiditySensor,
18384
+ outputCap: "humidity-sensor",
18385
+ outputField: "percent",
18386
+ quantity: "humidity",
18387
+ defaultAggregate: "mean",
18388
+ empty: "null",
18389
+ companions: [NOW]
18390
+ }),
18391
+ numeric({
18392
+ id: "pressure",
18393
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18394
+ role: DeviceRole.PressureSensor,
18395
+ outputCap: "pressure-sensor",
18396
+ outputField: "hpa",
18397
+ quantity: "pressure",
18398
+ defaultAggregate: "mean",
18399
+ empty: "null",
18400
+ companions: [NOW]
18401
+ }),
18402
+ numeric({
18403
+ id: "illuminance",
18404
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18405
+ role: DeviceRole.AmbientLightSensor,
18406
+ outputCap: "ambient-light-sensor",
18407
+ outputField: "lux",
18408
+ quantity: "illuminance",
18409
+ defaultAggregate: "mean",
18410
+ empty: "null",
18411
+ companions: [NOW]
18412
+ }),
18413
+ numeric({
18414
+ id: "power",
18415
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18416
+ role: DeviceRole.PowerSensor,
18417
+ outputCap: "power-meter",
18418
+ outputField: "watts",
18419
+ quantity: "power",
18420
+ defaultAggregate: "sum",
18421
+ empty: "omit",
18422
+ companions: [NOW]
18423
+ }),
18424
+ numeric({
18425
+ id: "energy",
18426
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18427
+ role: DeviceRole.EnergySensor,
18428
+ outputCap: "power-meter",
18429
+ outputField: "kwhTotal",
18430
+ quantity: "energy",
18431
+ defaultAggregate: "sum",
18432
+ empty: "omit",
18433
+ companions: [NOW]
18434
+ }),
18435
+ numeric({
18436
+ id: "battery",
18437
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18438
+ role: DeviceRole.BatterySensor,
18439
+ outputCap: "battery",
18440
+ outputField: "percentage",
18441
+ quantity: "percentage",
18442
+ defaultAggregate: "mean",
18443
+ empty: "null",
18444
+ companions: [
18445
+ {
18446
+ fieldPath: "charging",
18447
+ expr: "\"unknown\""
18448
+ },
18449
+ {
18450
+ fieldPath: "sleeping",
18451
+ expr: "false"
18452
+ },
18453
+ {
18454
+ fieldPath: "lastUpdated",
18455
+ expr: "now"
18456
+ }
18457
+ ]
18458
+ }),
18459
+ numeric({
18460
+ id: "numeric",
18461
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18462
+ role: DeviceRole.NumericSensor,
18463
+ outputCap: "numeric-sensor",
18464
+ outputField: "value",
18465
+ quantity: "device",
18466
+ defaultAggregate: "mean",
18467
+ empty: "null",
18468
+ companions: [NOW]
18469
+ }),
18470
+ binary({
18471
+ id: "motion",
18472
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18473
+ role: DeviceRole.MotionSensor,
18474
+ outputCap: "motion",
18475
+ outputField: "detected",
18476
+ defaultAggregate: "any",
18477
+ accept: [{
18478
+ cap: "motion",
18479
+ fieldPath: "detected"
18480
+ }],
18481
+ companions: [{
18482
+ fieldPath: "lastDetectedAt",
18483
+ expr: "null"
18484
+ }, {
18485
+ fieldPath: "autoClearAfterMs",
18486
+ expr: "null"
18487
+ }]
18488
+ }),
18489
+ binary({
18490
+ id: "contact",
18491
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18492
+ role: DeviceRole.ContactSensor,
18493
+ outputCap: "contact",
18494
+ outputField: "entryOpen",
18495
+ defaultAggregate: "any",
18496
+ accept: [{
18497
+ cap: "contact",
18498
+ fieldPath: "entryOpen"
18499
+ }],
18500
+ companions: [CHANGED]
18501
+ }),
18502
+ binary({
18503
+ id: "leak",
18504
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18505
+ role: DeviceRole.LeakSensor,
18506
+ outputCap: "flood",
18507
+ outputField: "flooded",
18508
+ defaultAggregate: "any",
18509
+ accept: [{
18510
+ cap: "flood",
18511
+ fieldPath: "flooded"
18512
+ }],
18513
+ companions: [CHANGED]
18514
+ }),
18515
+ binary({
18516
+ id: "smoke",
18517
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18518
+ role: DeviceRole.SmokeSensor,
18519
+ outputCap: "smoke",
18520
+ outputField: "detected",
18521
+ defaultAggregate: "any",
18522
+ accept: [{
18523
+ cap: "smoke",
18524
+ fieldPath: "detected"
18525
+ }],
18526
+ companions: [CHANGED]
18527
+ }),
18528
+ binary({
18529
+ id: "occupancy",
18530
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18531
+ role: DeviceRole.BinarySensor,
18532
+ outputCap: "binary",
18533
+ outputField: "on",
18534
+ defaultAggregate: "any",
18535
+ accept: [{
18536
+ cap: "motion",
18537
+ fieldPath: "detected"
18538
+ }, {
18539
+ cap: "binary",
18540
+ fieldPath: "on"
18541
+ }],
18542
+ companions: [CHANGED]
18543
+ }),
18544
+ binary({
18545
+ id: "binary",
18546
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18547
+ role: DeviceRole.BinarySensor,
18548
+ outputCap: "binary",
18549
+ outputField: "on",
18550
+ defaultAggregate: "any",
18551
+ accept: [{
18552
+ cap: "binary",
18553
+ fieldPath: "on"
18554
+ }],
18555
+ companions: [CHANGED]
18556
+ }),
18557
+ actuator({
18558
+ id: "switch",
18559
+ 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.",
18560
+ deviceType: DeviceType.Switch,
18561
+ outputCap: "switch",
18562
+ commandCaps: ["switch"]
18563
+ }),
18564
+ actuator({
18565
+ id: "light",
18566
+ 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.",
18567
+ deviceType: DeviceType.Light,
18568
+ outputCap: "switch",
18569
+ commandCaps: [
18570
+ "switch",
18571
+ "brightness",
18572
+ "color"
18573
+ ]
18574
+ })
18575
+ ];
18576
+ function actuator(input) {
18577
+ return {
18578
+ ...input,
18579
+ family: "actuator",
18580
+ role: void 0,
18581
+ memberDeviceType: input.deviceType,
18582
+ outputField: "on",
18583
+ quantity: "none",
18584
+ baseUnit: void 0,
18585
+ aggregates: BINARY_AGGREGATES,
18586
+ defaultAggregate: "any",
18587
+ empty: "offline",
18588
+ accept: [{
18589
+ cap: "switch",
18590
+ fieldPath: "on"
18591
+ }],
18592
+ companions: [CHANGED]
18593
+ };
18594
+ }
18595
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18596
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18209
18597
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18210
18598
  /**
18211
18599
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19919,12 +20307,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19919
20307
  * providers detect renames by matching `uniqueId` and patch `id`
19920
20308
  * in-place so dispatch keeps working.
19921
20309
  *
19922
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19923
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19924
- * state push and are the single source of truth. The `device_class` tag is
19925
- * an HA-specific input that is consumed at the discovery boundary
19926
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19927
- * stored after that point.
20310
+ * **Display unit and precision are NOT stored here.** They are a read-time
20311
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20312
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20313
+ * neither is repeated on each sample. The `device_class` tag is an
20314
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20315
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19928
20316
  *
19929
20317
  * The `raw` escape hatch carries provider-specific attributes that don't
19930
20318
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19938,6 +20326,14 @@ var SourceInfoSchema = object({
19938
20326
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19939
20327
  * Used to detect rename when `id` changes. */
19940
20328
  uniqueId: string().optional(),
20329
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20330
+ quantity: ReadingQuantitySchema.optional(),
20331
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20332
+ dateTimeShape: _enum([
20333
+ "date",
20334
+ "time",
20335
+ "datetime"
20336
+ ]).optional(),
19941
20337
  /** Provider-specific extras that don't fit the structured slots.
19942
20338
  * Whitelist what goes here per provider — do NOT dump entire HA
19943
20339
  * attribute blobs (would explode the meta JSON column). */
@@ -40961,7 +41357,8 @@ method(object({
40961
41357
  "enum"
40962
41358
  ]),
40963
41359
  enumValues: array(string()).optional(),
40964
- item: boolean().optional()
41360
+ item: boolean().optional(),
41361
+ quantity: ReadingQuantitySchema.optional()
40965
41362
  })).readonly(),
40966
41363
  itemArray: object({
40967
41364
  path: string(),
@@ -44805,8 +45202,8 @@ var MaskPolygonShapeSchema = object({
44805
45202
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44806
45203
  var MaskGridShapeSchema = object({
44807
45204
  kind: literal("grid"),
44808
- gridWidth: number(),
44809
- gridHeight: number(),
45205
+ gridWidth: number().meta(readingMeta("none")),
45206
+ gridHeight: number().meta(readingMeta("none")),
44810
45207
  cells: array(boolean())
44811
45208
  });
44812
45209
  discriminatedUnion("kind", [
@@ -44918,6 +45315,8 @@ var NcSystemEventKindSchema = _enum([
44918
45315
  "detection-blind",
44919
45316
  "motion-noisy",
44920
45317
  "motion-calm",
45318
+ "package-llm-silent",
45319
+ "package-llm-answered",
44921
45320
  "recording-stalled",
44922
45321
  "recording-healthy",
44923
45322
  "recording-degraded",
@@ -56157,6 +56556,28 @@ var PackageVersionInfoSchema = object({
56157
56556
  deprecated: string().optional(),
56158
56557
  distTags: array(string()).readonly()
56159
56558
  });
56559
+ /**
56560
+ * One published document. `ok` is markdown from the registry. `missing` means
56561
+ * the registry answered and the file was not there. `error` is a registry or
56562
+ * tarball failure — never an empty string standing in for either.
56563
+ */
56564
+ var PackageDocBodySchema = discriminatedUnion("status", [
56565
+ object({
56566
+ status: literal("ok"),
56567
+ markdown: string()
56568
+ }),
56569
+ object({ status: literal("missing") }),
56570
+ object({
56571
+ status: literal("error"),
56572
+ message: string()
56573
+ })
56574
+ ]);
56575
+ var PackageDocsSchema = object({
56576
+ name: string(),
56577
+ version: string(),
56578
+ readme: PackageDocBodySchema,
56579
+ changelog: PackageDocBodySchema
56580
+ });
56160
56581
  var SearchResultSchema = object({
56161
56582
  name: string(),
56162
56583
  version: string(),
@@ -56375,7 +56796,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56375
56796
  }), object({ success: literal(true) }), {
56376
56797
  kind: "mutation",
56377
56798
  auth: "admin"
56378
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56799
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56800
+ name: string().min(1),
56801
+ version: string().min(1).optional()
56802
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56379
56803
  kind: "mutation",
56380
56804
  auth: "admin"
56381
56805
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56434,27 +56858,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56434
56858
  */
56435
56859
  var AirQualitySensorStatusSchema = object({
56436
56860
  /** Carbon dioxide concentration in ppm. */
56437
- co2Ppm: number().min(0).optional(),
56861
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56438
56862
  /** Total volatile organic compounds in ppb. */
56439
- vocPpb: number().min(0).optional(),
56863
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56440
56864
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56441
- pm25: number().min(0).optional(),
56865
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56442
56866
  /** Particulate matter ≤ 10 μm in µg/m³. */
56443
- pm10: number().min(0).optional(),
56867
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56444
56868
  /** Composite AQI value (typically 0..500). */
56445
- aqi: number().optional(),
56869
+ aqi: number().optional().meta(readingMeta("index")),
56446
56870
  /** Ms epoch when the slice was last updated. */
56447
- lastFetchedAt: number(),
56448
- /** Live display unit of the single metric this slice carries (e.g. HA
56449
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56450
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56451
- * per slice is unambiguous. */
56452
- unit: string().optional(),
56453
- /** Suggested decimal places for numeric display.
56454
- * Populated live from the upstream source when provided (e.g. HA
56455
- * `attributes.suggested_display_precision`). Falls back to
56456
- * auto-formatting when absent. */
56457
- precision: number().int().min(0).max(10).optional()
56871
+ lastFetchedAt: number()
56458
56872
  });
56459
56873
  var airQualitySensorCapability = {
56460
56874
  name: "air-quality-sensor",
@@ -56491,12 +56905,12 @@ var airQualitySensorCapability = {
56491
56905
  },
56492
56906
  pm25: {
56493
56907
  kind: "gauge",
56494
- unit: "ug/m3",
56908
+ unit: "µg/m³",
56495
56909
  deadband: 1
56496
56910
  },
56497
56911
  pm10: {
56498
56912
  kind: "gauge",
56499
- unit: "ug/m3",
56913
+ unit: "µg/m³",
56500
56914
  deadband: 1
56501
56915
  },
56502
56916
  aqi: {
@@ -56514,22 +56928,13 @@ var airQualitySensorCapability = {
56514
56928
  * entries with `device_class: illuminance`.
56515
56929
  */
56516
56930
  var AmbientLightSensorStatusSchema = object({
56517
- /** Current illuminance in lux (lx). */
56518
- lux: number().min(0).meta({
56519
- unit: "lx",
56931
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56932
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56933
+ precision: 0,
56520
56934
  label: "Illuminance"
56521
- }),
56935
+ })),
56522
56936
  /** Ms epoch when the slice was last updated. */
56523
- lastFetchedAt: number(),
56524
- /** Live display unit from the upstream source (e.g. HA
56525
- * `attributes.unit_of_measurement`). The UI prefers this over the
56526
- * role's canonical unit. Absent → fall back to the canonical unit. */
56527
- unit: string().optional(),
56528
- /** Suggested decimal places for numeric display.
56529
- * Populated live from the upstream source when provided (e.g. HA
56530
- * `attributes.suggested_display_precision`). Falls back to
56531
- * auto-formatting when absent. */
56532
- precision: number().int().min(0).max(10).optional()
56937
+ lastFetchedAt: number()
56533
56938
  });
56534
56939
  var ambientLightSensorCapability = {
56535
56940
  name: "ambient-light-sensor",
@@ -56561,12 +56966,7 @@ var ambientLightSensorCapability = {
56561
56966
  } } },
56562
56967
  /** Clock fields: written, but excluded from the compare that decides
56563
56968
  * whether persisting is worth a SQLite commit. */
56564
- volatileStateFields: ["lastFetchedAt"],
56565
- customize: {
56566
- version: 1,
56567
- advanced: "derive",
56568
- targets: []
56569
- }
56969
+ volatileStateFields: ["lastFetchedAt"]
56570
56970
  };
56571
56971
  /**
56572
56972
  * Per-class audio metrics aggregated over a sliding window.
@@ -56793,10 +57193,7 @@ var BatteryStatusSchema = object({
56793
57193
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56794
57194
  * declines to band a non-finite reading, which is the correct shape.
56795
57195
  */
56796
- percentage: number().min(0).max(100).nullable().meta({
56797
- unit: "%",
56798
- label: "Charge"
56799
- }),
57196
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56800
57197
  /**
56801
57198
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56802
57199
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56823,7 +57220,14 @@ var BatteryStatusSchema = object({
56823
57220
  */
56824
57221
  sleeping: boolean().meta({ label: "Sleeping" }),
56825
57222
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56826
- lastUpdated: number(),
57223
+ lastUpdated: number().meta(readingMeta("none")),
57224
+ /**
57225
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57226
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57227
+ * that only report charge. A second reading on this cap, not a second
57228
+ * `numeric-sensor` on the same device.
57229
+ */
57230
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56827
57231
  /**
56828
57232
  * Ms epoch of the last time the device PROVED it was reachable — a
56829
57233
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56875,7 +57279,6 @@ var BATTERY_DEVICE_TYPES = [
56875
57279
  /** Generic Customize target for the percentage reading (D936). */
56876
57280
  var batteryCustomize = {
56877
57281
  version: 1,
56878
- advanced: "derive",
56879
57282
  targets: [{
56880
57283
  id: "percentage",
56881
57284
  cardinality: "scalar",
@@ -56884,7 +57287,7 @@ var batteryCustomize = {
56884
57287
  stateField: "percentage"
56885
57288
  },
56886
57289
  deviceTypes: BATTERY_DEVICE_TYPES,
56887
- apply: "add-or-replace",
57290
+ apply: "add",
56888
57291
  path: "percentage",
56889
57292
  sources: [{
56890
57293
  kind: "field",
@@ -57005,10 +57408,16 @@ onStatusChanged: { data: object({
57005
57408
  /**
57006
57409
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57007
57410
  */
57008
- history: { fields: { percentage: {
57009
- kind: "gauge",
57010
- unit: "%"
57011
- } } },
57411
+ history: { fields: {
57412
+ percentage: {
57413
+ kind: "gauge",
57414
+ unit: "%"
57415
+ },
57416
+ volts: {
57417
+ kind: "gauge",
57418
+ unit: "V"
57419
+ }
57420
+ } },
57012
57421
  customize: batteryCustomize,
57013
57422
  /** Clock fields: written, but excluded from the compare that decides
57014
57423
  * whether persisting is worth a SQLite commit. */
@@ -57055,12 +57464,7 @@ var binaryCapability = {
57055
57464
  history: { fields: { on: { kind: "boolean" } } },
57056
57465
  /** Clock fields: written, but excluded from the compare that decides
57057
57466
  * whether persisting is worth a SQLite commit. */
57058
- volatileStateFields: ["lastChangedAt"],
57059
- customize: {
57060
- version: 1,
57061
- advanced: "derive",
57062
- targets: []
57063
- }
57467
+ volatileStateFields: ["lastChangedAt"]
57064
57468
  };
57065
57469
  /**
57066
57470
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57076,7 +57480,7 @@ var binaryCapability = {
57076
57480
  */
57077
57481
  var BrightnessStatusSchema = object({
57078
57482
  /** Current level as 0..100 inclusive. Firmware-reported. */
57079
- percentage: number().min(0).max(100),
57483
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57080
57484
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57081
57485
  lastChangedAt: number()
57082
57486
  });
@@ -57530,21 +57934,21 @@ var ClimateControlStatusSchema = object({
57530
57934
  /** Single setpoint in Celsius. Used by single-target modes
57531
57935
  * (heat / cool / dry). Null when the device is in a range mode
57532
57936
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57533
- target: number().nullable(),
57937
+ target: number().nullable().meta(readingMeta("temperature")),
57534
57938
  /** Upper bound of the dual setpoint in Celsius. Populated only
57535
57939
  * in `heat_cool` mode. */
57536
- targetHigh: number().nullable(),
57940
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57537
57941
  /** Lower bound of the dual setpoint in Celsius. Populated only
57538
57942
  * in `heat_cool` mode. */
57539
- targetLow: number().nullable(),
57943
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57540
57944
  /** Target relative humidity (0..100). Null when the device has no
57541
57945
  * humidity control. */
57542
- targetHumidity: number().min(0).max(100).nullable(),
57946
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57543
57947
  /** Current measured temperature in Celsius. Read-only — pushed by
57544
57948
  * the device's internal sensor. */
57545
- currentTemp: number().nullable(),
57949
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57546
57950
  /** Current measured relative humidity (0..100). Read-only. */
57547
- currentHumidity: number().min(0).max(100).nullable(),
57951
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57548
57952
  /** Vertical louver swing on/off. Null when the device has no vertical
57549
57953
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57550
57954
  swingVertical: boolean().nullable(),
@@ -57552,11 +57956,11 @@ var ClimateControlStatusSchema = object({
57552
57956
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57553
57957
  swingHorizontal: boolean().nullable(),
57554
57958
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57555
- minTemp: number().nullable().optional(),
57959
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57556
57960
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57557
- maxTemp: number().nullable().optional(),
57961
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57558
57962
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57559
- tempStep: number().positive().nullable().optional(),
57963
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57560
57964
  /** Ms epoch when the slice was last updated (push or command). */
57561
57965
  lastFetchedAt: number()
57562
57966
  });
@@ -57698,17 +58102,17 @@ var climateControlCapability = {
57698
58102
  * modes to expose in the picker without a slice fetch.
57699
58103
  */
57700
58104
  var RgbTripletSchema = object({
57701
- r: number().int().min(0).max(255),
57702
- g: number().int().min(0).max(255),
57703
- b: number().int().min(0).max(255)
58105
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58106
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58107
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57704
58108
  });
57705
58109
  var HsvTripletSchema = object({
57706
58110
  /** Hue in degrees, 0..360. */
57707
- h: number().min(0).max(360),
58111
+ h: number().min(0).max(360).meta(readingMeta("none")),
57708
58112
  /** Saturation as 0..100 inclusive. */
57709
- s: number().min(0).max(100),
58113
+ s: number().min(0).max(100).meta(readingMeta("none")),
57710
58114
  /** Value/brightness as 0..100 inclusive. */
57711
- v: number().min(0).max(100)
58115
+ v: number().min(0).max(100).meta(readingMeta("none"))
57712
58116
  });
57713
58117
  var ColorInputSchema = discriminatedUnion("mode", [
57714
58118
  object({
@@ -57737,7 +58141,7 @@ var ColorStatusSchema = object({
57737
58141
  /** Populated when `mode === 'hsv'`. */
57738
58142
  hsv: HsvTripletSchema.optional(),
57739
58143
  /** Populated when `mode === 'mired'`. */
57740
- mireds: number().int().optional(),
58144
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57741
58145
  /** Ms epoch of the last operator-driven change. */
57742
58146
  lastChangedAt: number()
57743
58147
  });
@@ -57932,18 +58336,12 @@ _enum([
57932
58336
  "empty",
57933
58337
  "full"
57934
58338
  ]);
57935
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57936
- * each. */
57937
- var ConsumableRemainingUnitSchema = _enum([
57938
- "days",
57939
- "hours",
57940
- "cycles"
57941
- ]);
57942
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57943
- * days). `value` null = unknown (D393). */
58339
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58340
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58341
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57944
58342
  var ConsumableRemainingSchema = object({
57945
- value: number().min(0).nullable(),
57946
- unit: ConsumableRemainingUnitSchema
58343
+ value: number().min(0).nullable().meta(readingMeta("device")),
58344
+ kind: ConsumableRemainingKindSchema
57947
58345
  });
57948
58346
  /** A single consumable item. Either a continuous `level` (remaining
57949
58347
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57956,7 +58354,7 @@ var ConsumableItemSchema = object({
57956
58354
  /** Display name. */
57957
58355
  label: string().min(1),
57958
58356
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57959
- level: number().min(0).max(100).nullable(),
58357
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57960
58358
  /** The provider's discrete verdict when known (binary mode). */
57961
58359
  status: ConsumableStatusSchema.nullable(),
57962
58360
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58038,8 +58436,8 @@ var consumablesCustomize = {
58038
58436
  transforms: [{ kind: "identity" }]
58039
58437
  }],
58040
58438
  literals: [{
58041
- path: "remaining.unit",
58042
- values: [...ConsumableRemainingUnitSchema.options]
58439
+ path: "remaining.kind",
58440
+ values: [...ConsumableRemainingKindSchema.options]
58043
58441
  }]
58044
58442
  },
58045
58443
  {
@@ -58168,7 +58566,6 @@ reset: method(object({
58168
58566
  },
58169
58567
  "remaining.value": {
58170
58568
  kind: "gauge",
58171
- unitFrom: "remaining.unit",
58172
58569
  deadband: 1,
58173
58570
  displayName: "Remaining"
58174
58571
  }
@@ -58238,10 +58635,9 @@ var contactCapability = {
58238
58635
  * (empty array for the other kinds). It lives in the slice because
58239
58636
  * HA `select.*` entities may update their options at runtime.
58240
58637
  *
58241
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58242
- * `kind === 'numeric'` and are absent (undefined) for all other
58243
- * kinds. They are populated live from HA attributes on each state
58244
- * push — consistent with `options` and the sensor-unit approach.
58638
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58639
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58640
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58245
58641
  */
58246
58642
  var ControlStatusSchema = object({
58247
58643
  kind: _enum([
@@ -58253,39 +58649,17 @@ var ControlStatusSchema = object({
58253
58649
  /** Current value. Numeric kind → number; select/text/datetime → string.
58254
58650
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58255
58651
  * or time-only — HA accepts all three forms). */
58256
- value: union([number(), string()]),
58652
+ value: union([number().meta(readingMeta("device")), string()]),
58257
58653
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58258
58654
  options: array(string()),
58259
58655
  /** Ms epoch when the slice was last updated. */
58260
58656
  lastChangedAt: number(),
58261
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58262
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58263
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58264
- unit: string().optional(),
58265
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58266
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58267
- min: number().optional(),
58268
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58269
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58270
- max: number().optional(),
58271
- /** Step increment. Populated live from HA `attributes.step`.
58272
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58273
- step: number().optional(),
58274
- /** Suggested decimal places for numeric display. Populated live from HA
58275
- * `attributes.suggested_display_precision`, or derived from `step` when the
58276
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58277
- * other kinds. Falls back to auto-formatting when absent. */
58278
- precision: number().int().min(0).max(10).optional(),
58279
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58280
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58281
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58282
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58283
- * `has_date`/`has_time` attributes on each push. */
58284
- format: _enum([
58285
- "date",
58286
- "time",
58287
- "datetime"
58288
- ]).optional()
58657
+ /** Minimum allowed value, in the device quantity's base unit. */
58658
+ min: number().optional().meta(readingMeta("device")),
58659
+ /** Maximum allowed value, in the device quantity's base unit. */
58660
+ max: number().optional().meta(readingMeta("device")),
58661
+ /** Step increment, in the device quantity's base unit. */
58662
+ step: number().optional().meta(readingMeta("device"))
58289
58663
  });
58290
58664
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58291
58665
  object({
@@ -58351,9 +58725,9 @@ var CoverStatusSchema = object({
58351
58725
  ]),
58352
58726
  /** 0 = fully closed, 100 = fully open. Null when the device has
58353
58727
  * no intermediate position surface. */
58354
- position: number().min(0).max(100).nullable(),
58728
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58355
58729
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58356
- tiltPosition: number().min(0).max(100).nullable(),
58730
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58357
58731
  /** Ms epoch when the slice was last updated. */
58358
58732
  lastChangedAt: number()
58359
58733
  });
@@ -58456,9 +58830,9 @@ var NormalizedRangeSchema$1 = object({
58456
58830
  var DayNightStatusSchema = object({
58457
58831
  mode: DayNightModeSchema,
58458
58832
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58459
- sensitivity: number().optional(),
58833
+ sensitivity: number().optional().meta(readingMeta("none")),
58460
58834
  /** Delay before the IR-cut filter flips, in seconds. */
58461
- switchDelaySec: number().optional(),
58835
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58462
58836
  lastFetchedAt: number()
58463
58837
  });
58464
58838
  /**
@@ -58676,11 +59050,11 @@ var DishwasherStatusSchema = object({
58676
59050
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58677
59051
  availablePrograms: array(string()),
58678
59052
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58679
- remainingSeconds: number().int().nonnegative().nullable(),
59053
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58680
59054
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58681
- delayedStartSeconds: number().int().nonnegative().nullable(),
59055
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58682
59056
  /** Cycle completion 0..100, or null when not applicable. */
58683
- progressPercent: number().min(0).max(100).nullable(),
59057
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58684
59058
  /** True when the door is reported open; null when the door state is not known. */
58685
59059
  doorOpen: boolean().nullable(),
58686
59060
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58704,7 +59078,7 @@ var DishwasherStatusSchema = object({
58704
59078
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58705
59079
  machineCareDue: boolean().nullable(),
58706
59080
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58707
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59081
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58708
59082
  /** Option states: null when the appliance does not expose the option or has not said. */
58709
59083
  halfLoad: boolean().nullable(),
58710
59084
  hygienePlus: boolean().nullable(),
@@ -58719,7 +59093,7 @@ var DishwasherStatusSchema = object({
58719
59093
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58720
59094
  activeEvents: array(string()),
58721
59095
  /** Numeric error code when the appliance reports one; null otherwise. */
58722
- errorCode: number().nullable(),
59096
+ errorCode: number().nullable().meta(readingMeta("none")),
58723
59097
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58724
59098
  errorLabel: string().nullable(),
58725
59099
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58931,6 +59305,7 @@ var doorbellCustomize = {
58931
59305
  applyAuthority: { kind: "composition-claim" },
58932
59306
  eventName: "onPressed",
58933
59307
  edges: ["rising"],
59308
+ defaultEdge: "rising",
58934
59309
  timestampPath: "lastPressedAt",
58935
59310
  counterPath: "pressCountSinceStart",
58936
59311
  highWater: "composer-advance",
@@ -58949,7 +59324,7 @@ var DoorbellStatusSchema = object({
58949
59324
  /** Ms epoch of the last press. null = never observed since this provider started. */
58950
59325
  lastPressedAt: number().nullable(),
58951
59326
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58952
- pressCountSinceStart: number()
59327
+ pressCountSinceStart: number().meta(readingMeta("count"))
58953
59328
  });
58954
59329
  var DoorbellPressEventSchema = object({
58955
59330
  deviceId: number(),
@@ -59014,17 +59389,7 @@ onPressed: {
59014
59389
  history: "none",
59015
59390
  customize: doorbellCustomize
59016
59391
  };
59017
- /**
59018
- * Enum-state sensor — a string value picked from a finite option set.
59019
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59020
- * states, weather conditions, contact-source channels, mode strings).
59021
- * The option set is stable across observations; it is persisted in the
59022
- * device config blob (via the control slice at adoption) and is NOT
59023
- * stored in `sourceInfo`.
59024
- */
59025
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59026
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59027
- var EnumSensorDateTimeFormatSchema = _enum([
59392
+ _enum([
59028
59393
  "date",
59029
59394
  "time",
59030
59395
  "datetime"
@@ -59032,12 +59397,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59032
59397
  var EnumSensorStatusSchema = object({
59033
59398
  value: string(),
59034
59399
  /**
59035
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59036
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59037
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59038
- * a time-only sensor → `'time'`.
59400
+ * Set when `value` is a timestamp rather than an enum state, so history
59401
+ * does not learn ISO strings as states. The display shape (date / time /
59402
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59039
59403
  */
59040
- format: EnumSensorDateTimeFormatSchema.optional(),
59404
+ timestamp: boolean().optional(),
59041
59405
  /** Ms epoch when the slice was last updated. */
59042
59406
  lastFetchedAt: number()
59043
59407
  });
@@ -59064,11 +59428,11 @@ var enumSensorCapability = {
59064
59428
  * History (D850): the value as an `enum` series (an appliance's program or
59065
59429
  * operation state, an HVAC action). The option set is per device, so the
59066
59430
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59067
- * `format` carries a timestamp, not a state, and is not recorded.
59431
+ * `timestamp` carries a clock, not a state, and is not recorded.
59068
59432
  */
59069
59433
  history: { fields: { value: {
59070
59434
  kind: "enum",
59071
- skipWhenSet: "format"
59435
+ skipWhenSet: "timestamp"
59072
59436
  } } },
59073
59437
  /** Clock fields: written, but excluded from the compare that decides
59074
59438
  * whether persisting is worth a SQLite commit. */
@@ -59088,13 +59452,13 @@ var EventFireSchema = object({
59088
59452
  deviceId: number(),
59089
59453
  eventType: string(),
59090
59454
  data: record(string(), unknown()).nullable(),
59091
- timestamp: number(),
59092
- seq: number()
59455
+ timestamp: number().meta(readingMeta("none")),
59456
+ seq: number().meta(readingMeta("count"))
59093
59457
  });
59094
59458
  var EventEmitterStatusSchema = object({
59095
59459
  eventTypes: array(string()),
59096
59460
  lastEvent: EventFireSchema.nullable(),
59097
- eventCountSinceStart: number()
59461
+ eventCountSinceStart: number().meta(readingMeta("count"))
59098
59462
  });
59099
59463
  var eventEmitterCapability = {
59100
59464
  name: "event-emitter",
@@ -59507,10 +59871,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59507
59871
  var FanControlStatusSchema = object({
59508
59872
  /** Active speed as 0..100 inclusive. Null when the device has no
59509
59873
  * speed surface (single-speed fan). */
59510
- percentage: number().min(0).max(100).nullable(),
59874
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59511
59875
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59512
59876
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59513
- percentageStep: number().positive().optional(),
59877
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59514
59878
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59515
59879
  * — empty string when no preset is active or supported. */
59516
59880
  preset: string(),
@@ -59631,7 +59995,7 @@ var FeatureProbeStatusSchema = object({
59631
59995
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59632
59996
  model: string().nullable(),
59633
59997
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59634
- channelCount: number().nullable(),
59998
+ channelCount: number().nullable().meta(readingMeta("count")),
59635
59999
  /**
59636
60000
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59637
60001
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59754,9 +60118,91 @@ var gasCapability = {
59754
60118
  * whether persisting is worth a SQLite commit. */
59755
60119
  volatileStateFields: ["lastChangedAt"]
59756
60120
  };
60121
+ var GroupCommandsSchema = object({
60122
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60123
+ results: array(object({
60124
+ deviceId: number().int().nonnegative(),
60125
+ cap: string().min(1),
60126
+ method: string().min(1),
60127
+ ok: boolean(),
60128
+ error: string().optional(),
60129
+ /** Ms epoch of the attempt. */
60130
+ at: number()
60131
+ })) });
60132
+ var groupCommandsCapability = {
60133
+ name: "group-commands",
60134
+ scope: "device",
60135
+ deviceNative: true,
60136
+ mode: "singleton",
60137
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60138
+ methods: {},
60139
+ status: {
60140
+ schema: GroupCommandsSchema,
60141
+ kind: "push"
60142
+ },
60143
+ runtimeState: GroupCommandsSchema,
60144
+ /**
60145
+ * Runtime-state durability: **session** — the last attempt. A restored
60146
+ * failure would outlive the command that caused it.
60147
+ *
60148
+ * See `RuntimeStateDurability`. Enforced by
60149
+ * `scripts/check-runtime-state-durability.ts`.
60150
+ */
60151
+ durability: "session",
60152
+ history: "none"
60153
+ };
60154
+ /**
60155
+ * How many members of a helper group answered (D944). Counts are integers,
60156
+ * not measurements: zero included is a real count, never a stand-in reading.
60157
+ * The reading itself stays on the output capability. History is none — the
60158
+ * series that later statistics read is the output cap's own history block.
60159
+ */
60160
+ var GroupStatusSchema = object({
60161
+ /** Members whose current value entered the aggregate. */
60162
+ included: number().int().nonnegative().meta(readingMeta("count", {
60163
+ precision: 0,
60164
+ label: "Reporting"
60165
+ })),
60166
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60167
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60168
+ precision: 0,
60169
+ label: "Excluded"
60170
+ })),
60171
+ /** `included` + `excluded`. */
60172
+ total: number().int().nonnegative().meta(readingMeta("count", {
60173
+ precision: 0,
60174
+ label: "Members"
60175
+ }))
60176
+ });
60177
+ var groupStatusCapability = {
60178
+ name: "group-status",
60179
+ scope: "device",
60180
+ deviceNative: true,
60181
+ mode: "singleton",
60182
+ deviceTypes: [
60183
+ DeviceType.Sensor,
60184
+ DeviceType.Switch,
60185
+ DeviceType.Light
60186
+ ],
60187
+ methods: {},
60188
+ status: {
60189
+ schema: GroupStatusSchema,
60190
+ kind: "push"
60191
+ },
60192
+ runtimeState: GroupStatusSchema,
60193
+ /**
60194
+ * Runtime-state durability: **session** — recomputed from members on the
60195
+ * next source event; a restored count would outlive the readings it named.
60196
+ *
60197
+ * See `RuntimeStateDurability`. Enforced by
60198
+ * `scripts/check-runtime-state-durability.ts`.
60199
+ */
60200
+ durability: "session",
60201
+ history: "none"
60202
+ };
59757
60203
  var HeatingValveStatusSchema = object({
59758
60204
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59759
- position: number().min(0).max(100).nullable(),
60205
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59760
60206
  /** Ms epoch when the slice was last updated (push or poll). */
59761
60207
  lastFetchedAt: number()
59762
60208
  });
@@ -59799,9 +60245,9 @@ var HumidifierStatusSchema = object({
59799
60245
  /** Whether the humidifier is currently on. */
59800
60246
  on: boolean(),
59801
60247
  /** Current measured relative humidity (0..100). Null when not reported. */
59802
- currentHumidity: number().min(0).max(100).nullable(),
60248
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59803
60249
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59804
- targetHumidity: number().min(0).max(100).nullable(),
60250
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59805
60251
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59806
60252
  * device has no mode surface. */
59807
60253
  mode: string().nullable(),
@@ -59811,9 +60257,9 @@ var HumidifierStatusSchema = object({
59811
60257
  * `idle` / `off`). Null when the device doesn't report it. */
59812
60258
  action: string().nullable(),
59813
60259
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59814
- minHumidity: number().nullable(),
60260
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59815
60261
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59816
- maxHumidity: number().nullable(),
60262
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59817
60263
  /** Ms epoch when the slice was last updated. */
59818
60264
  lastChangedAt: number()
59819
60265
  });
@@ -59883,22 +60329,13 @@ var humidifierCapability = {
59883
60329
  * `sourceInfo`.
59884
60330
  */
59885
60331
  var HumiditySensorStatusSchema = object({
59886
- /** Current relative humidity, 0..100. */
59887
- percent: number().min(0).max(100).meta({
59888
- unit: "%",
60332
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60333
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60334
+ precision: 0,
59889
60335
  label: "Humidity"
59890
- }),
60336
+ })),
59891
60337
  /** Ms epoch when the slice was last updated. */
59892
- lastFetchedAt: number(),
59893
- /** Live display unit from the upstream source (e.g. HA
59894
- * `attributes.unit_of_measurement`). The UI prefers this over the
59895
- * role's canonical unit. Absent → fall back to the canonical unit. */
59896
- unit: string().optional(),
59897
- /** Suggested decimal places for numeric display.
59898
- * Populated live from the upstream source when provided (e.g. HA
59899
- * `attributes.suggested_display_precision`). Falls back to
59900
- * auto-formatting when absent. */
59901
- precision: number().int().min(0).max(10).optional()
60338
+ lastFetchedAt: number()
59902
60339
  });
59903
60340
  var humiditySensorCapability = {
59904
60341
  name: "humidity-sensor",
@@ -59929,12 +60366,7 @@ var humiditySensorCapability = {
59929
60366
  } } },
59930
60367
  /** Clock fields: written, but excluded from the compare that decides
59931
60368
  * whether persisting is worth a SQLite commit. */
59932
- volatileStateFields: ["lastFetchedAt"],
59933
- customize: {
59934
- version: 1,
59935
- advanced: "derive",
59936
- targets: []
59937
- }
60369
+ volatileStateFields: ["lastFetchedAt"]
59938
60370
  };
59939
60371
  /**
59940
60372
  * Image display cap. Models a single still image exposed by an integration —
@@ -59955,7 +60387,7 @@ var ImageStatusSchema = object({
59955
60387
  * entity exposes no `entity_picture` (yet). */
59956
60388
  url: string().nullable(),
59957
60389
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59958
- lastUpdated: number().nullable()
60390
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59959
60391
  });
59960
60392
  var imageCapability = {
59961
60393
  name: "image",
@@ -60043,19 +60475,19 @@ var NormalizedRangeSchema = object({
60043
60475
  */
60044
60476
  var ImageSettingsStatusSchema = object({
60045
60477
  /** Normalized 0–100. */
60046
- brightness: number().optional(),
60478
+ brightness: number().optional().meta(readingMeta("none")),
60047
60479
  /** Normalized 0–100. */
60048
- contrast: number().optional(),
60480
+ contrast: number().optional().meta(readingMeta("none")),
60049
60481
  /** Normalized 0–100. */
60050
- saturation: number().optional(),
60482
+ saturation: number().optional().meta(readingMeta("none")),
60051
60483
  /** Normalized 0–100. */
60052
- sharpness: number().optional(),
60484
+ sharpness: number().optional().meta(readingMeta("none")),
60053
60485
  mirror: boolean().optional(),
60054
60486
  flip: boolean().optional(),
60055
60487
  rotate: ImageRotateSchema.optional(),
60056
60488
  whiteBalance: WhiteBalanceModeSchema.optional(),
60057
60489
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60058
- warmth: number().optional(),
60490
+ warmth: number().optional().meta(readingMeta("none")),
60059
60491
  exposureMode: ExposureModeSchema.optional(),
60060
60492
  backlightMode: BacklightModeSchema.optional(),
60061
60493
  lastFetchedAt: number()
@@ -60334,10 +60766,10 @@ var IntercomAbilitySchema = object({
60334
60766
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60335
60767
  codecs: array(string()),
60336
60768
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60337
- sampleRate: number(),
60769
+ sampleRate: number().meta(readingMeta("none")),
60338
60770
  duplex: _enum(["half", "full"]),
60339
60771
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60340
- maxBacklogMs: number()
60772
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60341
60773
  });
60342
60774
  var IntercomStatusSchema = object({
60343
60775
  /** True while a talk session is open. */
@@ -60564,13 +60996,13 @@ var LawnMowerControlStatusSchema = object({
60564
60996
  activity: LawnMowerActivitySchema,
60565
60997
  /** 0..100 battery percentage. Null when the device has no battery
60566
60998
  * reading. */
60567
- batteryLevel: number().min(0).max(100).nullable(),
60999
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60568
61000
  /** 0..100 mowing-completion percentage of the current task, or null when no
60569
61001
  * task is active / progress is unavailable. */
60570
- progressPercent: number().min(0).max(100).nullable(),
61002
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60571
61003
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60572
61004
  * or null when unknown. */
60573
- currentCode: number().nullable(),
61005
+ currentCode: number().nullable().meta(readingMeta("none")),
60574
61006
  /** Human label for {@link currentCode}, or null when undecodable. */
60575
61007
  currentCodeLabel: string().nullable(),
60576
61008
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61103,7 +61535,7 @@ var MediaPlayerStatusSchema = object({
61103
61535
  state: MediaPlayerStateSchema,
61104
61536
  /** Volume as 0..100 inclusive. Null when the device has no volume
61105
61537
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61106
- volumeLevel: number().min(0).max(100).nullable(),
61538
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61107
61539
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61108
61540
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61109
61541
  isMuted: boolean().nullable(),
@@ -61116,10 +61548,10 @@ var MediaPlayerStatusSchema = object({
61116
61548
  currentMedia: MediaInfoSchema.nullable(),
61117
61549
  /** Current playback position in ms. Null when not seekable or
61118
61550
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61119
- positionMs: number().int().nonnegative().nullable(),
61551
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61120
61552
  /** Total duration of the current media in ms. Null when unknown
61121
61553
  * (live stream) or nothing is playing. */
61122
- durationMs: number().int().nonnegative().nullable(),
61554
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61123
61555
  /** Shuffle toggle. Null when no shuffle surface.
61124
61556
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61125
61557
  shuffle: boolean().nullable(),
@@ -61428,7 +61860,7 @@ var motionCustomize = {
61428
61860
  eventName: "onMotionChanged"
61429
61861
  },
61430
61862
  deviceTypes: [DeviceType.Camera],
61431
- apply: "replace",
61863
+ apply: "add",
61432
61864
  applyAuthority: {
61433
61865
  kind: "camera-sensors",
61434
61866
  timeoutField: "motionCooldownMs"
@@ -61500,7 +61932,7 @@ var MotionStatusSchema = object({
61500
61932
  * it, every provider seeded it at boot, and every session left it
61501
61933
  * `detected: true` after its detach.
61502
61934
  */
61503
- autoClearAfterMs: number().nullable()
61935
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61504
61936
  });
61505
61937
  /**
61506
61938
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61537,12 +61969,18 @@ var motionCapability = {
61537
61969
  methods: {
61538
61970
  /**
61539
61971
  * Pull the current motion state synchronously. Convenience shortcut
61540
- * for consumers that don't need the full status object; equivalent
61541
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61542
- * `getStatus` once the auto-injected status surface lands in every
61543
- * consumer.
61972
+ * for consumers that don't need the full status object. A composed
61973
+ * group answers from its slice (D944): unknown throws, it is not off.
61544
61974
  */
61545
61975
  isDetected: method(object({ deviceId: number() }), boolean()) },
61976
+ /**
61977
+ * A composed motion group serves `isDetected` from `detected` (D944).
61978
+ * Without this the cap stays uncomposable (`has-commands`).
61979
+ */
61980
+ composedMethods: { isDetected: {
61981
+ kind: "slice",
61982
+ field: "detected"
61983
+ } },
61546
61984
  events: {
61547
61985
  /**
61548
61986
  * Fires every time the runner transitions a camera between
@@ -61682,7 +62120,7 @@ var MotionZoneRegionSchema = object({
61682
62120
  * the grid region(s). */
61683
62121
  var MotionZoneStatusSchema = object({
61684
62122
  enabled: boolean(),
61685
- sensitivity: number(),
62123
+ sensitivity: number().meta(readingMeta("none")),
61686
62124
  /** Grid region(s). Today exactly one `grid` shape. */
61687
62125
  regions: array(MotionZoneRegionSchema),
61688
62126
  lastFetchedAt: number()
@@ -62052,7 +62490,7 @@ var NavigationStatusSchema = object({
62052
62490
  following: boolean(),
62053
62491
  flash: boolean(),
62054
62492
  lightMode: NavigationLightModeSchema,
62055
- lightLevel: number().min(40).max(100),
62493
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62056
62494
  /** Ms epoch when the slice was last updated. */
62057
62495
  lastChangedAt: number()
62058
62496
  });
@@ -62186,13 +62624,13 @@ var NetworkLinkStatusSchema = object({
62186
62624
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62187
62625
  * SKIP a null rather than coerce it.
62188
62626
  */
62189
- signalPercent: number().min(0).max(100).nullable(),
62627
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62190
62628
  /** Raw received signal strength in dBm, when the firmware reports one. */
62191
- rssiDbm: number().optional(),
62629
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62192
62630
  /** Network name of a wireless link, when the firmware reports it. */
62193
62631
  ssid: string().optional(),
62194
62632
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62195
- lastUpdated: number()
62633
+ lastUpdated: number().meta(readingMeta("none"))
62196
62634
  });
62197
62635
  var networkLinkCapability = {
62198
62636
  name: "network-link",
@@ -62577,7 +63015,7 @@ var NotifierStatusSchema = object({
62577
63015
  lastError: string().nullable(),
62578
63016
  /** Number of deliveries currently buffered server-side (rate-limited
62579
63017
  * notifiers may queue). 0 when the provider sends synchronously. */
62580
- queueDepth: number().int().nonnegative(),
63018
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62581
63019
  /** Per-feature capability matrix — authoritative source for the
62582
63020
  * compose-form field gating. */
62583
63021
  supports: NotifierSupportsSchema
@@ -62669,11 +63107,9 @@ onSent: { data: object({
62669
63107
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62670
63108
  * for: water flow, distance, weight, frequency, signal strength, …).
62671
63109
  *
62672
- * The `unit` and `precision` fields carry the live display metadata that
62673
- * the upstream source provides with each state push (HA
62674
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62675
- * The UI reads them directly from the slice — they are the single source
62676
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63110
+ * The quantity is not on this slice. It is chosen when the device is
63111
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63112
+ * that quantity's base unit. `null` is a refused sample (D393).
62677
63113
  *
62678
63114
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62679
63115
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62682,15 +63118,8 @@ onSent: { data: object({
62682
63118
  * numeric values land here.
62683
63119
  */
62684
63120
  var NumericSensorStatusSchema = object({
62685
- value: number(),
62686
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62687
- * Populated live from the upstream source on each state push.
62688
- * Absent for unitless sensors. */
62689
- unit: string().optional(),
62690
- /** Suggested decimal places for numeric display.
62691
- * Populated live from the upstream source when provided.
62692
- * Falls back to auto-formatting when absent. */
62693
- precision: number().int().min(0).max(10).optional(),
63121
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63122
+ value: number().nullable().meta(readingMeta("device")),
62694
63123
  /** Ms epoch when the slice was last updated. */
62695
63124
  lastFetchedAt: number()
62696
63125
  });
@@ -62714,11 +63143,11 @@ var numericSensorCapability = {
62714
63143
  */
62715
63144
  durability: "restored",
62716
63145
  /**
62717
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63146
+ * History (D850): recorded series. The unit is the device quantity's base,
63147
+ * resolved when the series is read — the cap has no single unit.
62718
63148
  */
62719
63149
  history: { fields: { value: {
62720
63150
  kind: "gauge",
62721
- unitFrom: "unit",
62722
63151
  minIntervalMs: 3e4
62723
63152
  } } },
62724
63153
  /** Clock fields: written, but excluded from the compare that decides
@@ -63010,23 +63439,26 @@ var PetFeederStatusSchema = object({
63010
63439
  /** Food currently in the bowl (grams). Null when the device has not
63011
63440
  * reported a reading yet. On dual-hopper models this is the combined
63012
63441
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63013
- foodLevel: number().nullable(),
63442
+ foodLevel: number().nullable().meta(readingMeta("none")),
63014
63443
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63015
63444
  * single-hopper models. */
63016
- food1: number().nullable(),
63445
+ food1: number().nullable().meta(readingMeta("none")),
63017
63446
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63018
63447
  * single-hopper models. */
63019
- food2: number().nullable(),
63448
+ food2: number().nullable().meta(readingMeta("none")),
63020
63449
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63021
63450
  * (`device_class: problem`, on = low). True when the bowl is empty /
63022
63451
  * below the feeder's low threshold. */
63023
63452
  lowFood: boolean(),
63024
63453
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63025
63454
  * device has no battery reading. */
63026
- batteryPower: number().min(0).max(100).nullable(),
63027
- /** Days of desiccant life remaining. Null when the model has no
63028
- * desiccant sensor. */
63029
- desiccantLeftDays: number().nullable(),
63455
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63456
+ /**
63457
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63458
+ * Providers convert from the days the feeder reports. Null when the
63459
+ * model has no desiccant sensor.
63460
+ */
63461
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63030
63462
  /** True while a feed is in progress. */
63031
63463
  feeding: boolean(),
63032
63464
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63037,7 +63469,7 @@ var PetFeederStatusSchema = object({
63037
63469
  * with `errorCode` for consumers that want the raw integer. */
63038
63470
  error: string().nullable(),
63039
63471
  /** Raw device error code (0 / null = no error). */
63040
- errorCode: number().nullable(),
63472
+ errorCode: number().nullable().meta(readingMeta("none")),
63041
63473
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63042
63474
  isDualHopper: boolean(),
63043
63475
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63047,7 +63479,7 @@ var PetFeederStatusSchema = object({
63047
63479
  /** Play a chime when dispensing. */
63048
63480
  feedSound: boolean(),
63049
63481
  /** Speaker / prompt volume level (device-scaled integer). */
63050
- volume: number(),
63482
+ volume: number().meta(readingMeta("none")),
63051
63483
  /** Ms epoch when the slice was last refreshed from the cloud. */
63052
63484
  lastFetchedAt: number()
63053
63485
  });
@@ -63166,7 +63598,7 @@ var petFeederCapability = {
63166
63598
  },
63167
63599
  desiccantLeftDays: {
63168
63600
  kind: "gauge",
63169
- unit: "days"
63601
+ unit: "s"
63170
63602
  }
63171
63603
  } }
63172
63604
  };
@@ -63567,36 +63999,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63567
63999
  */
63568
64000
  var PowerMeterStatusSchema = object({
63569
64001
  /** Instantaneous power draw in watts. */
63570
- watts: number().optional().meta({
63571
- unit: "W",
63572
- label: "Power"
63573
- }),
64002
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63574
64003
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63575
- kwhTotal: number().optional().meta({
63576
- unit: "kWh",
63577
- label: "Energy"
63578
- }),
64004
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63579
64005
  /** Voltage in volts. */
63580
- volts: number().optional(),
64006
+ volts: number().optional().meta(readingMeta("voltage")),
63581
64007
  /** Current in amperes. */
63582
- amps: number().optional(),
63583
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63584
- va: number().optional(),
64008
+ amps: number().optional().meta(readingMeta("current")),
64009
+ /** Apparent power in volt-amperes. */
64010
+ va: number().optional().meta(readingMeta("apparent-power")),
63585
64011
  /** Power factor (0..1) if reported. */
63586
- powerFactor: number().min(0).max(1).optional(),
64012
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63587
64013
  /** Ms epoch when the slice was last updated. */
63588
- lastFetchedAt: number(),
63589
- /** Live display unit of the single metric this slice carries (e.g. HA
63590
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63591
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63592
- * unit per slice is unambiguous. The UI prefers this over the role's
63593
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63594
- unit: string().optional(),
63595
- /** Suggested decimal places for numeric display.
63596
- * Populated live from the upstream source when provided (e.g. HA
63597
- * `attributes.suggested_display_precision`). Falls back to
63598
- * auto-formatting when absent. */
63599
- precision: number().int().min(0).max(10).optional()
64014
+ lastFetchedAt: number()
63600
64015
  });
63601
64016
  var powerMeterCapability = {
63602
64017
  name: "power-meter",
@@ -63660,12 +64075,7 @@ var powerMeterCapability = {
63660
64075
  } },
63661
64076
  /** Clock fields: written, but excluded from the compare that decides
63662
64077
  * whether persisting is worth a SQLite commit. */
63663
- volatileStateFields: ["lastFetchedAt"],
63664
- customize: {
63665
- version: 1,
63666
- advanced: "derive",
63667
- targets: []
63668
- }
64078
+ volatileStateFields: ["lastFetchedAt"]
63669
64079
  };
63670
64080
  /**
63671
64081
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63685,11 +64095,11 @@ var powerMeterCapability = {
63685
64095
  */
63686
64096
  var GpsLocationSchema = object({
63687
64097
  /** Latitude in decimal degrees, -90..90. */
63688
- latitude: number().min(-90).max(90),
64098
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63689
64099
  /** Longitude in decimal degrees, -180..180. */
63690
- longitude: number().min(-180).max(180),
64100
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63691
64101
  /** Reported accuracy in meters (lower = better). */
63692
- accuracyMeters: number().nonnegative()
64102
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63693
64103
  });
63694
64104
  var PresenceStatusSchema = object({
63695
64105
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63701,7 +64111,7 @@ var PresenceStatusSchema = object({
63701
64111
  gps: GpsLocationSchema.nullable(),
63702
64112
  /** Optional battery level of the tracking device (0..100). Null
63703
64113
  * when not reported. */
63704
- batteryPercent: number().min(0).max(100).nullable(),
64114
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63705
64115
  /** Ms epoch when the slice was last updated. */
63706
64116
  lastChangedAt: number()
63707
64117
  });
@@ -63751,19 +64161,10 @@ var presenceCapability = {
63751
64161
  * `sourceInfo`.
63752
64162
  */
63753
64163
  var PressureSensorStatusSchema = object({
63754
- /** Current pressure in hPa. */
63755
- hpa: number(),
64164
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64165
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63756
64166
  /** Ms epoch when the slice was last updated. */
63757
- lastFetchedAt: number(),
63758
- /** Live display unit from the upstream source (e.g. HA
63759
- * `attributes.unit_of_measurement`). The UI prefers this over the
63760
- * role's canonical unit. Absent → fall back to the canonical unit. */
63761
- unit: string().optional(),
63762
- /** Suggested decimal places for numeric display.
63763
- * Populated live from the upstream source when provided (e.g. HA
63764
- * `attributes.suggested_display_precision`). Falls back to
63765
- * auto-formatting when absent. */
63766
- precision: number().int().min(0).max(10).optional()
64167
+ lastFetchedAt: number()
63767
64168
  });
63768
64169
  var pressureSensorCapability = {
63769
64170
  name: "pressure-sensor",
@@ -64177,8 +64578,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64177
64578
  }), _void(), { kind: "mutation" });
64178
64579
  var PtzAutotrackSettingsSchema = object({
64179
64580
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64180
- stopDelaySeconds: number().int().min(0).max(300),
64181
- disappearDelaySeconds: number().int().min(0).max(300)
64581
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64582
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64182
64583
  });
64183
64584
  /**
64184
64585
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64332,7 +64733,7 @@ var RunningStateStatusSchema = object({
64332
64733
  /** null = unknown — never coerced to false (D393). */
64333
64734
  running: boolean().nullable(),
64334
64735
  /** Epoch ms the current boolean began; null while `running` is null. */
64335
- since: number().nullable(),
64736
+ since: number().nullable().meta(readingMeta("none")),
64336
64737
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64337
64738
  phase: string().nullable(),
64338
64739
  /** null until a reading has been published (D393). */
@@ -64347,7 +64748,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64347
64748
  /** Generic Customize target for the running boolean (D936). */
64348
64749
  var runningCustomize = {
64349
64750
  version: 1,
64350
- advanced: "derive",
64351
64751
  targets: [{
64352
64752
  id: "running",
64353
64753
  cardinality: "scalar",
@@ -64367,7 +64767,7 @@ var runningCustomize = {
64367
64767
  DeviceType.Thermostat,
64368
64768
  DeviceType.PetFeeder
64369
64769
  ],
64370
- apply: "add-or-replace",
64770
+ apply: "add",
64371
64771
  path: "running",
64372
64772
  sources: [
64373
64773
  {
@@ -64383,6 +64783,7 @@ var runningCustomize = {
64383
64783
  requiredUnit: "W",
64384
64784
  transforms: [{
64385
64785
  kind: "threshold",
64786
+ comparisons: ["gte", "lte"],
64386
64787
  comparison: "gte",
64387
64788
  defaultThreshold: 5,
64388
64789
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -65630,9 +66031,9 @@ var RecordTriggerSchema = _enum([
65630
66031
  */
65631
66032
  var RecordWindowSchema = object({
65632
66033
  trigger: RecordTriggerSchema,
65633
- day: number().int().min(0).max(6),
65634
- startMinute: number().int().min(0).max(1439),
65635
- endMinute: number().int().min(1).max(1440)
66034
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66035
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66036
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65636
66037
  });
65637
66038
  /** Status of one physical volume, as the camera itself describes it. */
65638
66039
  var OnboardStorageVolumeSchema = object({
@@ -65708,10 +66109,10 @@ var RecordingOnboardStatusSchema = object({
65708
66109
  /** Master "record to the card at all" switch. */
65709
66110
  enabled: boolean().nullable(),
65710
66111
  overwriteWhenFull: boolean().nullable(),
65711
- preRecordSec: number().nullable(),
65712
- postRecordSec: number().nullable(),
66112
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66113
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65713
66114
  /** Length of one recorded file, in minutes. */
65714
- segmentMinutes: number().nullable(),
66115
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65715
66116
  /** The primary track's weekly windows, flattened. */
65716
66117
  windows: array(RecordWindowSchema),
65717
66118
  /**
@@ -65727,7 +66128,7 @@ var RecordingOnboardStatusSchema = object({
65727
66128
  * provider reporting a non-zero count also reports the schedule as not
65728
66129
  * writable.
65729
66130
  */
65730
- unreadableWindows: number(),
66131
+ unreadableWindows: number().meta(readingMeta("count")),
65731
66132
  /**
65732
66133
  * The camera is scheduled to record and has NO usable storage.
65733
66134
  *
@@ -66572,7 +66973,7 @@ var CamStreamDescriptorSchema = object({
66572
66973
  url: string().optional(),
66573
66974
  codec: string().optional(),
66574
66975
  resolution: CamStreamResolutionSchema.optional(),
66575
- fps: number().positive().optional(),
66976
+ fps: number().positive().optional().meta(readingMeta("rate")),
66576
66977
  label: string().optional(),
66577
66978
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66578
66979
  deviceFeatures: array(string()).optional(),
@@ -66659,18 +67060,18 @@ var StreamProfileSchema = _enum([
66659
67060
  ]);
66660
67061
  /** The current encoding config of one stream profile. */
66661
67062
  var StreamProfileConfigSchema = object({
66662
- width: number(),
66663
- height: number(),
67063
+ width: number().meta(readingMeta("none")),
67064
+ height: number().meta(readingMeta("none")),
66664
67065
  codec: _enum(["h264", "h265"]),
66665
- framerate: number(),
66666
- bitrate: number(),
67066
+ framerate: number().meta(readingMeta("rate")),
67067
+ bitrate: number().meta(readingMeta("bitrate")),
66667
67068
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66668
67069
  encoderProfile: _enum([
66669
67070
  "high",
66670
67071
  "main",
66671
67072
  "baseline"
66672
67073
  ]).optional(),
66673
- gop: number().optional(),
67074
+ gop: number().optional().meta(readingMeta("none")),
66674
67075
  /**
66675
67076
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66676
67077
  *
@@ -66891,12 +67292,7 @@ var switchCapability = {
66891
67292
  label: "On",
66892
67293
  kind: "boolean"
66893
67294
  }
66894
- }] },
66895
- customize: {
66896
- version: 1,
66897
- advanced: "derive",
66898
- targets: []
66899
- }
67295
+ }] }
66900
67296
  };
66901
67297
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66902
67298
  function hasControlCharacter(value) {
@@ -67479,35 +67875,21 @@ var tamperCapability = {
67479
67875
  * entries with `device_class: temperature` and any future native
67480
67876
  * thermometer.
67481
67877
  *
67482
- * Unit normalisation: providers convert to Celsius before storing.
67483
- * The slice value is always Celsius so cross-cap aggregators
67484
- * (climate-control's `currentTemp`, energy analytics) can compose
67485
- * without per-source unit fixups. The canonical display unit (`°C`) is
67486
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67487
- * `sourceInfo`.
67878
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67879
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67488
67880
  *
67489
67881
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67490
67882
  * frozen feed (provider hung) distinct from a "temperature hasn't
67491
67883
  * changed" steady state.
67492
67884
  */
67493
67885
  var TemperatureSensorStatusSchema = object({
67494
- /** Current temperature in Celsius. */
67495
- celsius: number().meta({
67496
- unit: "°C",
67886
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67887
+ celsius: number().nullable().meta(readingMeta("temperature", {
67888
+ precision: 1,
67497
67889
  label: "Temperature"
67498
- }),
67890
+ })),
67499
67891
  /** Ms epoch when the slice was last updated (push or poll). */
67500
- lastFetchedAt: number(),
67501
- /** Live display unit from the upstream source (e.g. HA
67502
- * `attributes.unit_of_measurement`). The UI prefers this over the
67503
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67504
- * Absent → the UI falls back to the role's canonical unit. */
67505
- unit: string().optional(),
67506
- /** Suggested decimal places for numeric display.
67507
- * Populated live from the upstream source when provided (e.g. HA
67508
- * `attributes.suggested_display_precision`). Falls back to
67509
- * auto-formatting when absent. */
67510
- precision: number().int().min(0).max(10).optional()
67892
+ lastFetchedAt: number()
67511
67893
  });
67512
67894
  var temperatureSensorCapability = {
67513
67895
  name: "temperature-sensor",
@@ -67538,12 +67920,7 @@ var temperatureSensorCapability = {
67538
67920
  } } },
67539
67921
  /** Clock fields: written, but excluded from the compare that decides
67540
67922
  * whether persisting is worth a SQLite commit. */
67541
- volatileStateFields: ["lastFetchedAt"],
67542
- customize: {
67543
- version: 1,
67544
- advanced: "derive",
67545
- targets: []
67546
- }
67923
+ volatileStateFields: ["lastFetchedAt"]
67547
67924
  };
67548
67925
  /**
67549
67926
  * toast — system-scoped singleton capability that streams toast
@@ -67826,16 +68203,16 @@ var VacuumControlStatusSchema = object({
67826
68203
  ]),
67827
68204
  /** 0..100 battery percentage. Null when the device has no battery
67828
68205
  * reading. */
67829
- batteryLevel: number().min(0).max(100).nullable(),
68206
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67830
68207
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67831
68208
  * the vacuum has no speed control. */
67832
68209
  fanSpeed: string().nullable(),
67833
68210
  /** Speed tokens the hardware accepts — drives the UI selector. */
67834
68211
  availableFanSpeeds: array(string()),
67835
68212
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67836
- progressPercent: number().min(0).max(100).nullable(),
68213
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67837
68214
  /** Current error code (0 / null = no error). */
67838
- errorCode: number().nullable(),
68215
+ errorCode: number().nullable().meta(readingMeta("none")),
67839
68216
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67840
68217
  errorLabel: string().nullable(),
67841
68218
  /** Ms epoch when the slice was last updated. */
@@ -67913,7 +68290,7 @@ var ValveStatusSchema = object({
67913
68290
  ]),
67914
68291
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67915
68292
  * intermediate position surface. */
67916
- position: number().min(0).max(100).nullable(),
68293
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67917
68294
  /** Ms epoch when the slice was last updated. */
67918
68295
  lastChangedAt: number()
67919
68296
  });
@@ -68021,9 +68398,9 @@ var vibrationCapability = {
68021
68398
  */
68022
68399
  var WaterHeaterStatusSchema = object({
68023
68400
  /** Current measured temperature. Null when not reported. */
68024
- currentTemp: number().nullable(),
68401
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68025
68402
  /** Target temperature setpoint. Null when no setpoint surface. */
68026
- targetTemp: number().nullable(),
68403
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68027
68404
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68028
68405
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68029
68406
  * device reports an unknown state. */
@@ -68034,9 +68411,9 @@ var WaterHeaterStatusSchema = object({
68034
68411
  * has no away surface. */
68035
68412
  away: boolean().nullable(),
68036
68413
  /** HA `min_temp` attribute. Null when not reported. */
68037
- minTemp: number().nullable(),
68414
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68038
68415
  /** HA `max_temp` attribute. Null when not reported. */
68039
- maxTemp: number().nullable(),
68416
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68040
68417
  /** Ms epoch when the slice was last updated. */
68041
68418
  lastChangedAt: number()
68042
68419
  });
@@ -68121,25 +68498,18 @@ var waterHeaterCapability = {
68121
68498
  * Forecast deferred — current conditions only for v1.
68122
68499
  */
68123
68500
  var WeatherStatusSchema = object({
68124
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68125
- * when no condition has been reported yet. */
68501
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68126
68502
  condition: string().nullable(),
68127
- /** Current temperature in the reported unit. Null when not provided. */
68128
- temperature: number().nullable(),
68129
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68130
- temperatureUnit: string().nullable(),
68503
+ /** Current temperature in Celsius. Null when not provided or refused. */
68504
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68131
68505
  /** Relative humidity (0..100). Null when not provided. */
68132
- humidity: number().min(0).max(100).nullable(),
68133
- /** Barometric pressure in the reported unit. Null when not provided. */
68134
- pressure: number().nullable(),
68135
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68136
- pressureUnit: string().nullable(),
68137
- /** Wind speed in the reported unit. Null when not provided. */
68138
- windSpeed: number().nullable(),
68139
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68140
- windSpeedUnit: string().nullable(),
68506
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68507
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68508
+ pressure: number().nullable().meta(readingMeta("pressure")),
68509
+ /** Wind speed in metres per second. Null when not provided or refused. */
68510
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68141
68511
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68142
- windBearing: number().nullable(),
68512
+ windBearing: number().nullable().meta(readingMeta("angle")),
68143
68513
  /** Ms epoch when the slice was last updated. */
68144
68514
  lastFetchedAt: number()
68145
68515
  });
@@ -68167,12 +68537,12 @@ var weatherCapability = {
68167
68537
  */
68168
68538
  durability: "session",
68169
68539
  /**
68170
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68540
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68171
68541
  */
68172
68542
  history: { fields: {
68173
68543
  temperature: {
68174
68544
  kind: "gauge",
68175
- unitFrom: "temperatureUnit",
68545
+ unit: "°C",
68176
68546
  deadband: .2
68177
68547
  },
68178
68548
  humidity: {
@@ -68182,12 +68552,12 @@ var weatherCapability = {
68182
68552
  },
68183
68553
  pressure: {
68184
68554
  kind: "gauge",
68185
- unitFrom: "pressureUnit",
68555
+ unit: "hPa",
68186
68556
  deadband: .5
68187
68557
  },
68188
68558
  windSpeed: {
68189
68559
  kind: "gauge",
68190
- unitFrom: "windSpeedUnit",
68560
+ unit: "m/s",
68191
68561
  deadband: .5
68192
68562
  }
68193
68563
  } }
@@ -69418,6 +69788,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69418
69788
  featureProbe: featureProbeCapability,
69419
69789
  flood: floodCapability,
69420
69790
  gas: gasCapability,
69791
+ groupCommands: groupCommandsCapability,
69792
+ groupStatus: groupStatusCapability,
69421
69793
  heatingValve: heatingValveCapability,
69422
69794
  humidifier: humidifierCapability,
69423
69795
  humiditySensor: humiditySensorCapability,
@@ -70311,6 +70683,13 @@ Object.freeze({
70311
70683
  access: "view",
70312
70684
  role: "admin"
70313
70685
  },
70686
+ "addons.getPackageDocs": {
70687
+ capName: "addons",
70688
+ capScope: "system",
70689
+ addonId: null,
70690
+ access: "view",
70691
+ role: "admin"
70692
+ },
70314
70693
  "addons.getVersions": {
70315
70694
  capName: "addons",
70316
70695
  capScope: "system",
@@ -73419,6 +73798,20 @@ Object.freeze({
73419
73798
  access: "view",
73420
73799
  role: "viewer"
73421
73800
  },
73801
+ "groupCommands.getStatus": {
73802
+ capName: "group-commands",
73803
+ capScope: "device",
73804
+ addonId: null,
73805
+ access: "view",
73806
+ role: "viewer"
73807
+ },
73808
+ "groupStatus.getStatus": {
73809
+ capName: "group-status",
73810
+ capScope: "device",
73811
+ addonId: null,
73812
+ access: "view",
73813
+ role: "viewer"
73814
+ },
73422
73815
  "heatingValve.getStatus": {
73423
73816
  capName: "heating-valve",
73424
73817
  capScope: "device",
@@ -83241,6 +83634,37 @@ var SENSOR_CAP_VOCABULARY = {
83241
83634
  icon: "flame"
83242
83635
  }]
83243
83636
  },
83637
+ "group-commands": {
83638
+ scope: "device",
83639
+ noun: t("Group command", "Comando di gruppo")
83640
+ },
83641
+ "group-status": {
83642
+ scope: "device",
83643
+ noun: t("Group", "Gruppo"),
83644
+ state: [
83645
+ {
83646
+ kind: "numeric",
83647
+ field: "included",
83648
+ quantity: t("the members reporting", "i membri che rispondono"),
83649
+ label: t("Reporting", "Rispondono"),
83650
+ icon: "users"
83651
+ },
83652
+ {
83653
+ kind: "numeric",
83654
+ field: "excluded",
83655
+ quantity: t("the members left out", "i membri esclusi"),
83656
+ label: t("Excluded", "Esclusi"),
83657
+ icon: "users"
83658
+ },
83659
+ {
83660
+ kind: "numeric",
83661
+ field: "total",
83662
+ quantity: t("the member count", "il numero di membri"),
83663
+ label: t("Members", "Membri"),
83664
+ icon: "sigma"
83665
+ }
83666
+ ]
83667
+ },
83244
83668
  "humidity-sensor": {
83245
83669
  scope: "device",
83246
83670
  noun: t("Humidity", "Umidità"),
@@ -83274,7 +83698,6 @@ var SENSOR_CAP_VOCABULARY = {
83274
83698
  field: "value",
83275
83699
  quantity: t("the reading", "il valore"),
83276
83700
  label: t("Reading", "Valore"),
83277
- unitField: "unit",
83278
83701
  icon: "gauge"
83279
83702
  }]
83280
83703
  },
@@ -83410,7 +83833,7 @@ var SENSOR_CAP_VOCABULARY = {
83410
83833
  field: "temperature",
83411
83834
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83412
83835
  label: t("Temperature", "Temperatura"),
83413
- unitField: "temperatureUnit",
83836
+ unit: "°C",
83414
83837
  icon: "thermometer"
83415
83838
  },
83416
83839
  {
@@ -83426,7 +83849,7 @@ var SENSOR_CAP_VOCABULARY = {
83426
83849
  field: "windSpeed",
83427
83850
  quantity: t("the wind speed", "la velocità del vento"),
83428
83851
  label: t("Wind", "Vento"),
83429
- unitField: "windSpeedUnit",
83852
+ unit: "m/s",
83430
83853
  icon: "wind"
83431
83854
  },
83432
83855
  {
@@ -83434,7 +83857,7 @@ var SENSOR_CAP_VOCABULARY = {
83434
83857
  field: "pressure",
83435
83858
  quantity: t("the air pressure", "la pressione atmosferica"),
83436
83859
  label: t("Pressure", "Pressione"),
83437
- unitField: "pressureUnit",
83860
+ unit: "hPa",
83438
83861
  icon: "gauge"
83439
83862
  }
83440
83863
  ]
@@ -84707,6 +85130,16 @@ Object.freeze({
84707
85130
  form: "single",
84708
85131
  optional: false
84709
85132
  }],
85133
+ "groupCommands.getStatus": [{
85134
+ name: "deviceId",
85135
+ form: "single",
85136
+ optional: false
85137
+ }],
85138
+ "groupStatus.getStatus": [{
85139
+ name: "deviceId",
85140
+ form: "single",
85141
+ optional: false
85142
+ }],
84710
85143
  "heatingValve.getStatus": [{
84711
85144
  name: "deviceId",
84712
85145
  form: "single",
@@ -86571,6 +87004,11 @@ var PulseOverrideDraftSchema = object({
86571
87004
  targetId: string().min(1),
86572
87005
  enabled: boolean(),
86573
87006
  debounceMs: number().nonnegative(),
87007
+ edges: array(_enum([
87008
+ "rising",
87009
+ "button",
87010
+ "native-event"
87011
+ ])).min(1).optional(),
86574
87012
  sources: array(OverrideSourceSchema).min(1)
86575
87013
  });
86576
87014
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -86686,6 +87124,8 @@ new Map(Object.entries({
86686
87124
  "detection-blind": "device-events",
86687
87125
  "motion-noisy": "device-events",
86688
87126
  "motion-calm": "device-events",
87127
+ "package-llm-silent": "device-events",
87128
+ "package-llm-answered": "device-events",
86689
87129
  "recording-stalled": "device-events",
86690
87130
  "recording-healthy": "device-events",
86691
87131
  "recording-degraded": "device-events",
@@ -86769,6 +87209,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
86769
87209
  label: "Motion without a subject",
86770
87210
  kinds: ["motion-noisy", "motion-calm"]
86771
87211
  },
87212
+ {
87213
+ id: "package-llm",
87214
+ family: "device-events",
87215
+ label: "Package confirm",
87216
+ kinds: ["package-llm-silent", "package-llm-answered"]
87217
+ },
86772
87218
  {
86773
87219
  id: "recording",
86774
87220
  family: "device-events",
@@ -86950,6 +87396,8 @@ var DEVICE_EVENTS = [
86950
87396
  "detection-blind",
86951
87397
  "motion-noisy",
86952
87398
  "motion-calm",
87399
+ "package-llm-silent",
87400
+ "package-llm-answered",
86953
87401
  "recording-stalled",
86954
87402
  "recording-healthy",
86955
87403
  "recording-degraded",
@@ -87218,6 +87666,15 @@ new Map([
87218
87666
  snapshotLabel: "Cameras with constant motion now",
87219
87667
  snapshotSection: null
87220
87668
  },
87669
+ {
87670
+ id: "package-llm-confirm",
87671
+ family: "device-events",
87672
+ label: "Package confirm (not answering / answering again)",
87673
+ bad: ["package-llm-silent"],
87674
+ recovery: ["package-llm-answered"],
87675
+ snapshotLabel: "Cameras whose package confirm is not answering",
87676
+ snapshotSection: null
87677
+ },
87221
87678
  {
87222
87679
  id: "recording-health",
87223
87680
  family: "device-events",