@camstack/addon-provider-reolink 1.2.210 → 1.2.211

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
@@ -36,7 +36,7 @@ let node_os = require("node:os");
36
36
  node_os = require_chunk.__toESM(node_os, 1);
37
37
  let node_fs = require("node:fs");
38
38
  node_fs = require_chunk.__toESM(node_fs, 1);
39
- //#region ../types/dist/event-category-DHXxB8t9.mjs
39
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
40
40
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
41
41
  EventCategory["SystemBoot"] = "system.boot";
42
42
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -362,6 +362,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
362
362
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
363
363
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
364
364
  /**
365
+ * A camera's package LLM confirm has failed closed three times in a row
366
+ * (D931, 2026-10-26). One edge per camera until
367
+ * {@link PipelinePackageLlmAnswered}.
368
+ */
369
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
370
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
371
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
372
+ /**
365
373
  * Per-camera pipeline config was mutated by the orchestrator
366
374
  * (a settings change via `setAgentInferenceDevices` /
367
375
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5471,7 +5479,7 @@ var ZodIssueCode = {
5471
5479
  var ZodFirstPartyTypeKind;
5472
5480
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5473
5481
  //#endregion
5474
- //#region ../types/dist/sleep-BErAxhul.mjs
5482
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5475
5483
  /**
5476
5484
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5477
5485
  * window to float samples (D455).
@@ -8029,6 +8037,99 @@ var RecordKindDeclarationSchema = object({
8029
8037
  }).strict();
8030
8038
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8031
8039
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8040
+ var READING_QUANTITIES = [
8041
+ "temperature",
8042
+ "pressure",
8043
+ "speed",
8044
+ "precipitation",
8045
+ "precipitation-rate",
8046
+ "length",
8047
+ "area",
8048
+ "illuminance",
8049
+ "irradiance",
8050
+ "power",
8051
+ "energy",
8052
+ "voltage",
8053
+ "current",
8054
+ "apparent-power",
8055
+ "humidity",
8056
+ "percentage",
8057
+ "concentration-volume",
8058
+ "concentration-mass",
8059
+ "concentration-ppb",
8060
+ "volume",
8061
+ "duration",
8062
+ "duration-ms",
8063
+ "information",
8064
+ "sound-level",
8065
+ "signal-level",
8066
+ "colour-temperature",
8067
+ "angle",
8068
+ "bitrate",
8069
+ "rate",
8070
+ "mass",
8071
+ "index",
8072
+ "ratio",
8073
+ "count",
8074
+ "none",
8075
+ "device"
8076
+ ];
8077
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8078
+ new Set(READING_QUANTITIES);
8079
+ /**
8080
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8081
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8082
+ */
8083
+ var QUANTITY_BASE = {
8084
+ temperature: "°C",
8085
+ pressure: "hPa",
8086
+ speed: "m/s",
8087
+ precipitation: "mm",
8088
+ "precipitation-rate": "mm/h",
8089
+ length: "m",
8090
+ area: "m²",
8091
+ illuminance: "lx",
8092
+ irradiance: "W/m²",
8093
+ power: "W",
8094
+ energy: "kWh",
8095
+ voltage: "V",
8096
+ current: "A",
8097
+ "apparent-power": "VA",
8098
+ humidity: "%",
8099
+ percentage: "%",
8100
+ "concentration-volume": "ppm",
8101
+ "concentration-mass": "µg/m³",
8102
+ "concentration-ppb": "ppb",
8103
+ volume: "L",
8104
+ duration: "s",
8105
+ "duration-ms": "ms",
8106
+ information: "B",
8107
+ "sound-level": "dBFS",
8108
+ "signal-level": "dBm",
8109
+ "colour-temperature": "mired",
8110
+ angle: "°",
8111
+ bitrate: "kbps",
8112
+ rate: "fps",
8113
+ mass: "kg",
8114
+ index: "none",
8115
+ ratio: "none",
8116
+ count: "none",
8117
+ none: "none",
8118
+ device: "device"
8119
+ };
8120
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8121
+ function baseUnitOf(quantity) {
8122
+ const symbol = QUANTITY_BASE[quantity];
8123
+ if (symbol === "none" || symbol === "device") return void 0;
8124
+ return symbol;
8125
+ }
8126
+ function readingMeta(quantity, extras) {
8127
+ return {
8128
+ quantity,
8129
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8130
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8131
+ };
8132
+ }
8032
8133
  /** Shared Zod schemas used across streaming capabilities. */
8033
8134
  var CamProfileSchema = _enum([
8034
8135
  "high",
@@ -8056,8 +8157,8 @@ var CamStreamKindSchema = _enum([
8056
8157
  "derived"
8057
8158
  ]);
8058
8159
  var CamStreamResolutionSchema = object({
8059
- width: number().int().positive(),
8060
- height: number().int().positive()
8160
+ width: number().int().positive().meta(readingMeta("none")),
8161
+ height: number().int().positive().meta(readingMeta("none"))
8061
8162
  });
8062
8163
  var CameraStreamSchema = object({
8063
8164
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8609,6 +8710,8 @@ var CAP_NAMES = Object.freeze([
8609
8710
  "filesystem-browse",
8610
8711
  "flood",
8611
8712
  "gas",
8713
+ "group-commands",
8714
+ "group-status",
8612
8715
  "heating-valve",
8613
8716
  "humidifier",
8614
8717
  "humidity-sensor",
@@ -8738,14 +8841,6 @@ Object.freeze({
8738
8841
  {
8739
8842
  "path": "aqi",
8740
8843
  "valueType": "number"
8741
- },
8742
- {
8743
- "path": "unit",
8744
- "valueType": "string"
8745
- },
8746
- {
8747
- "path": "precision",
8748
- "valueType": "number"
8749
8844
  }
8750
8845
  ],
8751
8846
  "alarm-panel": [
@@ -8781,20 +8876,10 @@ Object.freeze({
8781
8876
  "valueType": "boolean"
8782
8877
  }
8783
8878
  ],
8784
- "ambient-light-sensor": [
8785
- {
8786
- "path": "lux",
8787
- "valueType": "number"
8788
- },
8789
- {
8790
- "path": "unit",
8791
- "valueType": "string"
8792
- },
8793
- {
8794
- "path": "precision",
8795
- "valueType": "number"
8796
- }
8797
- ],
8879
+ "ambient-light-sensor": [{
8880
+ "path": "lux",
8881
+ "valueType": "number"
8882
+ }],
8798
8883
  "audio-metrics": [
8799
8884
  {
8800
8885
  "path": "ts",
@@ -8874,6 +8959,10 @@ Object.freeze({
8874
8959
  "path": "lastUpdated",
8875
8960
  "valueType": "number"
8876
8961
  },
8962
+ {
8963
+ "path": "volts",
8964
+ "valueType": "number"
8965
+ },
8877
8966
  {
8878
8967
  "path": "lastContactAt",
8879
8968
  "valueType": "number"
@@ -9116,10 +9205,6 @@ Object.freeze({
9116
9205
  "datetime"
9117
9206
  ]
9118
9207
  },
9119
- {
9120
- "path": "unit",
9121
- "valueType": "string"
9122
- },
9123
9208
  {
9124
9209
  "path": "min",
9125
9210
  "valueType": "number"
@@ -9131,19 +9216,6 @@ Object.freeze({
9131
9216
  {
9132
9217
  "path": "step",
9133
9218
  "valueType": "number"
9134
- },
9135
- {
9136
- "path": "precision",
9137
- "valueType": "number"
9138
- },
9139
- {
9140
- "path": "format",
9141
- "valueType": "enum",
9142
- "enumValues": [
9143
- "date",
9144
- "time",
9145
- "datetime"
9146
- ]
9147
9219
  }
9148
9220
  ],
9149
9221
  "cover": [
@@ -9384,13 +9456,8 @@ Object.freeze({
9384
9456
  "path": "value",
9385
9457
  "valueType": "string"
9386
9458
  }, {
9387
- "path": "format",
9388
- "valueType": "enum",
9389
- "enumValues": [
9390
- "date",
9391
- "time",
9392
- "datetime"
9393
- ]
9459
+ "path": "timestamp",
9460
+ "valueType": "boolean"
9394
9461
  }],
9395
9462
  "event-emitter": [
9396
9463
  {
@@ -9463,6 +9530,20 @@ Object.freeze({
9463
9530
  "path": "detected",
9464
9531
  "valueType": "boolean"
9465
9532
  }],
9533
+ "group-status": [
9534
+ {
9535
+ "path": "included",
9536
+ "valueType": "number"
9537
+ },
9538
+ {
9539
+ "path": "excluded",
9540
+ "valueType": "number"
9541
+ },
9542
+ {
9543
+ "path": "total",
9544
+ "valueType": "number"
9545
+ }
9546
+ ],
9466
9547
  "heating-valve": [{
9467
9548
  "path": "position",
9468
9549
  "valueType": "number"
@@ -9497,20 +9578,10 @@ Object.freeze({
9497
9578
  "valueType": "number"
9498
9579
  }
9499
9580
  ],
9500
- "humidity-sensor": [
9501
- {
9502
- "path": "percent",
9503
- "valueType": "number"
9504
- },
9505
- {
9506
- "path": "unit",
9507
- "valueType": "string"
9508
- },
9509
- {
9510
- "path": "precision",
9511
- "valueType": "number"
9512
- }
9513
- ],
9581
+ "humidity-sensor": [{
9582
+ "path": "percent",
9583
+ "valueType": "number"
9584
+ }],
9514
9585
  "image": [{
9515
9586
  "path": "url",
9516
9587
  "valueType": "string"
@@ -9849,20 +9920,10 @@ Object.freeze({
9849
9920
  "valueType": "boolean"
9850
9921
  }
9851
9922
  ],
9852
- "numeric-sensor": [
9853
- {
9854
- "path": "value",
9855
- "valueType": "number"
9856
- },
9857
- {
9858
- "path": "unit",
9859
- "valueType": "string"
9860
- },
9861
- {
9862
- "path": "precision",
9863
- "valueType": "number"
9864
- }
9865
- ],
9923
+ "numeric-sensor": [{
9924
+ "path": "value",
9925
+ "valueType": "number"
9926
+ }],
9866
9927
  "pet-feeder": [
9867
9928
  {
9868
9929
  "path": "foodLevel",
@@ -9954,14 +10015,6 @@ Object.freeze({
9954
10015
  {
9955
10016
  "path": "powerFactor",
9956
10017
  "valueType": "number"
9957
- },
9958
- {
9959
- "path": "unit",
9960
- "valueType": "string"
9961
- },
9962
- {
9963
- "path": "precision",
9964
- "valueType": "number"
9965
10018
  }
9966
10019
  ],
9967
10020
  "presence": [
@@ -9990,20 +10043,10 @@ Object.freeze({
9990
10043
  "valueType": "number"
9991
10044
  }
9992
10045
  ],
9993
- "pressure-sensor": [
9994
- {
9995
- "path": "hpa",
9996
- "valueType": "number"
9997
- },
9998
- {
9999
- "path": "unit",
10000
- "valueType": "string"
10001
- },
10002
- {
10003
- "path": "precision",
10004
- "valueType": "number"
10005
- }
10006
- ],
10046
+ "pressure-sensor": [{
10047
+ "path": "hpa",
10048
+ "valueType": "number"
10049
+ }],
10007
10050
  "privacy-mask": [{
10008
10051
  "path": "enabled",
10009
10052
  "valueType": "boolean"
@@ -10314,20 +10357,10 @@ Object.freeze({
10314
10357
  "path": "tampered",
10315
10358
  "valueType": "boolean"
10316
10359
  }],
10317
- "temperature-sensor": [
10318
- {
10319
- "path": "celsius",
10320
- "valueType": "number"
10321
- },
10322
- {
10323
- "path": "unit",
10324
- "valueType": "string"
10325
- },
10326
- {
10327
- "path": "precision",
10328
- "valueType": "number"
10329
- }
10330
- ],
10360
+ "temperature-sensor": [{
10361
+ "path": "celsius",
10362
+ "valueType": "number"
10363
+ }],
10331
10364
  "update": [
10332
10365
  {
10333
10366
  "path": "currentVersion",
@@ -10438,10 +10471,6 @@ Object.freeze({
10438
10471
  "path": "temperature",
10439
10472
  "valueType": "number"
10440
10473
  },
10441
- {
10442
- "path": "temperatureUnit",
10443
- "valueType": "string"
10444
- },
10445
10474
  {
10446
10475
  "path": "humidity",
10447
10476
  "valueType": "number"
@@ -10450,18 +10479,10 @@ Object.freeze({
10450
10479
  "path": "pressure",
10451
10480
  "valueType": "number"
10452
10481
  },
10453
- {
10454
- "path": "pressureUnit",
10455
- "valueType": "string"
10456
- },
10457
10482
  {
10458
10483
  "path": "windSpeed",
10459
10484
  "valueType": "number"
10460
10485
  },
10461
- {
10462
- "path": "windSpeedUnit",
10463
- "valueType": "string"
10464
- },
10465
10486
  {
10466
10487
  "path": "windBearing",
10467
10488
  "valueType": "number"
@@ -10892,7 +10913,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10892
10913
  return DeviceRole;
10893
10914
  }({});
10894
10915
  //#endregion
10895
- //#region ../types/dist/composition-kEl9mevu.mjs
10916
+ //#region ../types/dist/composition-OhpuespA.mjs
10896
10917
  /**
10897
10918
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10898
10919
  *
@@ -11647,15 +11668,74 @@ function validateExpressionSource(src) {
11647
11668
  }
11648
11669
  return null;
11649
11670
  }
11650
- /** A composed device's stableId is this prefix plus the block id. */
11651
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11652
- /** Longest snippet a `code` source may carry (slice 4). */
11653
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11671
+ /**
11672
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11673
+ * id. Split out so helper schemas can name it without a cycle through
11674
+ * `composition.ts` (D659).
11675
+ */
11654
11676
  var CompositionSourceRefSchema = object({
11655
11677
  /** The owning addon; `stableId` is unique only within it. */
11656
11678
  addonId: string().min(1),
11657
11679
  stableId: string().min(1)
11658
11680
  });
11681
+ var GROUP_TEMPLATE_IDS = [
11682
+ "temperature",
11683
+ "humidity",
11684
+ "pressure",
11685
+ "illuminance",
11686
+ "power",
11687
+ "energy",
11688
+ "battery",
11689
+ "numeric",
11690
+ "motion",
11691
+ "contact",
11692
+ "leak",
11693
+ "smoke",
11694
+ "occupancy",
11695
+ "binary",
11696
+ "switch",
11697
+ "light"
11698
+ ];
11699
+ var NUMERIC_AGGREGATES = [
11700
+ "mean",
11701
+ "median",
11702
+ "min",
11703
+ "max",
11704
+ "sum"
11705
+ ];
11706
+ var BINARY_AGGREGATES = ["any", "all"];
11707
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11708
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11709
+ var AGGREGATE_REDUCERS = [
11710
+ "mean",
11711
+ "median",
11712
+ "min",
11713
+ "max",
11714
+ "sum",
11715
+ "any",
11716
+ "all"
11717
+ ];
11718
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11719
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11720
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11721
+ /** One member. The field is stored so occupancy can name motion or binary. */
11722
+ var HelperMemberSchema = object({
11723
+ source: CompositionSourceRefSchema,
11724
+ cap: string().min(1),
11725
+ fieldPath: string().min(1)
11726
+ });
11727
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11728
+ kind: literal("group"),
11729
+ templateId: GroupTemplateIdSchema,
11730
+ aggregate: AggregateReducerSchema,
11731
+ members: array(HelperMemberSchema).min(1).max(64),
11732
+ /** Actuator groups only. Readings leave this unset. */
11733
+ active: ActiveCriterionSchema.optional()
11734
+ })]);
11735
+ /** A composed device's stableId is this prefix plus the block id. */
11736
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11737
+ /** Longest snippet a `code` source may carry (slice 4). */
11738
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11659
11739
  /** One field of one capability of one source device. */
11660
11740
  var CompositionFieldReadSchema = object({
11661
11741
  source: CompositionSourceRefSchema,
@@ -11699,7 +11779,33 @@ var CompositionCodeSourceSchema = object({
11699
11779
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11700
11780
  CompositionFromSourceSchema,
11701
11781
  CompositionExpressionSourceSchema,
11702
- CompositionCodeSourceSchema
11782
+ CompositionCodeSourceSchema,
11783
+ object({
11784
+ kind: literal("aggregate"),
11785
+ reducer: _enum([
11786
+ "mean",
11787
+ "median",
11788
+ "min",
11789
+ "max",
11790
+ "sum",
11791
+ "any",
11792
+ "all"
11793
+ ]),
11794
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11795
+ /** `value` is the reading; the others are the member counts. */
11796
+ publish: _enum([
11797
+ "value",
11798
+ "included",
11799
+ "excluded",
11800
+ "total"
11801
+ ]),
11802
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11803
+ empty: _enum([
11804
+ "null",
11805
+ "omit",
11806
+ "offline"
11807
+ ])
11808
+ })
11703
11809
  ]);
11704
11810
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11705
11811
  kind: literal("forward"),
@@ -11745,7 +11851,13 @@ var CompositionSchema = object({
11745
11851
  kind: literal("existing"),
11746
11852
  device: CompositionSourceRefSchema
11747
11853
  })]),
11748
- features: array(CompositionFeatureSchema).min(1).max(16)
11854
+ features: array(CompositionFeatureSchema).min(1).max(16),
11855
+ /**
11856
+ * Present when the block is a helper template. Absent on a free
11857
+ * composition (the room example, device 4603). The editor reads this,
11858
+ * never the compiled features.
11859
+ */
11860
+ helper: HelperSpecSchema.optional()
11749
11861
  }).superRefine((composition, ctx) => {
11750
11862
  const seen = /* @__PURE__ */ new Set();
11751
11863
  composition.features.forEach((feature, index) => {
@@ -18523,7 +18635,11 @@ var CompositionProblemSchema = object({
18523
18635
  "item-field-reserved",
18524
18636
  "target-missing",
18525
18637
  "target-unreadable",
18526
- "not-replaceable"
18638
+ "not-replaceable",
18639
+ "native-capability-replace",
18640
+ "mixed-quantity",
18641
+ "helper-mismatch",
18642
+ "member-constraint"
18527
18643
  ]),
18528
18644
  path: string(),
18529
18645
  message: string()
@@ -18544,7 +18660,8 @@ var CompositionUnavailableModeSchema = _enum([
18544
18660
  "null",
18545
18661
  "offline",
18546
18662
  "unknown",
18547
- "release"
18663
+ "release",
18664
+ "omit"
18548
18665
  ]);
18549
18666
  /**
18550
18667
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18581,9 +18698,11 @@ var CompositionFieldPlanSchema = object({
18581
18698
  reads: array(CompositionFieldReadSchema)
18582
18699
  });
18583
18700
  /**
18584
- * What a feature does to its target (D663): `new` builds a capability on a new
18585
- * device; on an existing device it `add`s a capability the device lacks, or
18586
- * `replace`s named fields of one it has natively.
18701
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18702
+ * new device; on an existing device it `add`s a capability the device lacks.
18703
+ * Replacing a capability the device already serves is refused
18704
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18705
+ * wire for a claim the hub already holds.
18587
18706
  */
18588
18707
  var CompositionFeatureModeSchema = _enum([
18589
18708
  "new",
@@ -18756,6 +18875,275 @@ Object.freeze({
18756
18875
  result: ["number", "null"]
18757
18876
  }
18758
18877
  });
18878
+ /**
18879
+ * One catalog row per base reading a group helper can build (D944 §3).
18880
+ * Data, not a form default written into the block.
18881
+ */
18882
+ var NOW = {
18883
+ fieldPath: "lastFetchedAt",
18884
+ expr: "now"
18885
+ };
18886
+ var CHANGED = {
18887
+ fieldPath: "lastChangedAt",
18888
+ expr: "now"
18889
+ };
18890
+ function numeric(input) {
18891
+ return {
18892
+ ...input,
18893
+ family: "numeric",
18894
+ deviceType: DeviceType.Sensor,
18895
+ memberDeviceType: DeviceType.Sensor,
18896
+ commandCaps: [],
18897
+ baseUnit: baseUnitOf(input.quantity),
18898
+ aggregates: NUMERIC_AGGREGATES,
18899
+ accept: [{
18900
+ cap: input.outputCap,
18901
+ fieldPath: input.outputField
18902
+ }]
18903
+ };
18904
+ }
18905
+ function binary(input) {
18906
+ return {
18907
+ ...input,
18908
+ family: "binary",
18909
+ deviceType: DeviceType.Sensor,
18910
+ memberDeviceType: DeviceType.Sensor,
18911
+ commandCaps: [],
18912
+ quantity: "none",
18913
+ baseUnit: void 0,
18914
+ aggregates: BINARY_AGGREGATES,
18915
+ empty: "offline"
18916
+ };
18917
+ }
18918
+ var GROUP_TEMPLATES = [
18919
+ numeric({
18920
+ id: "temperature",
18921
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18922
+ role: DeviceRole.TemperatureSensor,
18923
+ outputCap: "temperature-sensor",
18924
+ outputField: "celsius",
18925
+ quantity: "temperature",
18926
+ defaultAggregate: "mean",
18927
+ empty: "null",
18928
+ companions: [NOW]
18929
+ }),
18930
+ numeric({
18931
+ id: "humidity",
18932
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18933
+ role: DeviceRole.HumiditySensor,
18934
+ outputCap: "humidity-sensor",
18935
+ outputField: "percent",
18936
+ quantity: "humidity",
18937
+ defaultAggregate: "mean",
18938
+ empty: "null",
18939
+ companions: [NOW]
18940
+ }),
18941
+ numeric({
18942
+ id: "pressure",
18943
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18944
+ role: DeviceRole.PressureSensor,
18945
+ outputCap: "pressure-sensor",
18946
+ outputField: "hpa",
18947
+ quantity: "pressure",
18948
+ defaultAggregate: "mean",
18949
+ empty: "null",
18950
+ companions: [NOW]
18951
+ }),
18952
+ numeric({
18953
+ id: "illuminance",
18954
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18955
+ role: DeviceRole.AmbientLightSensor,
18956
+ outputCap: "ambient-light-sensor",
18957
+ outputField: "lux",
18958
+ quantity: "illuminance",
18959
+ defaultAggregate: "mean",
18960
+ empty: "null",
18961
+ companions: [NOW]
18962
+ }),
18963
+ numeric({
18964
+ id: "power",
18965
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18966
+ role: DeviceRole.PowerSensor,
18967
+ outputCap: "power-meter",
18968
+ outputField: "watts",
18969
+ quantity: "power",
18970
+ defaultAggregate: "sum",
18971
+ empty: "omit",
18972
+ companions: [NOW]
18973
+ }),
18974
+ numeric({
18975
+ id: "energy",
18976
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18977
+ role: DeviceRole.EnergySensor,
18978
+ outputCap: "power-meter",
18979
+ outputField: "kwhTotal",
18980
+ quantity: "energy",
18981
+ defaultAggregate: "sum",
18982
+ empty: "omit",
18983
+ companions: [NOW]
18984
+ }),
18985
+ numeric({
18986
+ id: "battery",
18987
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18988
+ role: DeviceRole.BatterySensor,
18989
+ outputCap: "battery",
18990
+ outputField: "percentage",
18991
+ quantity: "percentage",
18992
+ defaultAggregate: "mean",
18993
+ empty: "null",
18994
+ companions: [
18995
+ {
18996
+ fieldPath: "charging",
18997
+ expr: "\"unknown\""
18998
+ },
18999
+ {
19000
+ fieldPath: "sleeping",
19001
+ expr: "false"
19002
+ },
19003
+ {
19004
+ fieldPath: "lastUpdated",
19005
+ expr: "now"
19006
+ }
19007
+ ]
19008
+ }),
19009
+ numeric({
19010
+ id: "numeric",
19011
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19012
+ role: DeviceRole.NumericSensor,
19013
+ outputCap: "numeric-sensor",
19014
+ outputField: "value",
19015
+ quantity: "device",
19016
+ defaultAggregate: "mean",
19017
+ empty: "null",
19018
+ companions: [NOW]
19019
+ }),
19020
+ binary({
19021
+ id: "motion",
19022
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19023
+ role: DeviceRole.MotionSensor,
19024
+ outputCap: "motion",
19025
+ outputField: "detected",
19026
+ defaultAggregate: "any",
19027
+ accept: [{
19028
+ cap: "motion",
19029
+ fieldPath: "detected"
19030
+ }],
19031
+ companions: [{
19032
+ fieldPath: "lastDetectedAt",
19033
+ expr: "null"
19034
+ }, {
19035
+ fieldPath: "autoClearAfterMs",
19036
+ expr: "null"
19037
+ }]
19038
+ }),
19039
+ binary({
19040
+ id: "contact",
19041
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19042
+ role: DeviceRole.ContactSensor,
19043
+ outputCap: "contact",
19044
+ outputField: "entryOpen",
19045
+ defaultAggregate: "any",
19046
+ accept: [{
19047
+ cap: "contact",
19048
+ fieldPath: "entryOpen"
19049
+ }],
19050
+ companions: [CHANGED]
19051
+ }),
19052
+ binary({
19053
+ id: "leak",
19054
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19055
+ role: DeviceRole.LeakSensor,
19056
+ outputCap: "flood",
19057
+ outputField: "flooded",
19058
+ defaultAggregate: "any",
19059
+ accept: [{
19060
+ cap: "flood",
19061
+ fieldPath: "flooded"
19062
+ }],
19063
+ companions: [CHANGED]
19064
+ }),
19065
+ binary({
19066
+ id: "smoke",
19067
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19068
+ role: DeviceRole.SmokeSensor,
19069
+ outputCap: "smoke",
19070
+ outputField: "detected",
19071
+ defaultAggregate: "any",
19072
+ accept: [{
19073
+ cap: "smoke",
19074
+ fieldPath: "detected"
19075
+ }],
19076
+ companions: [CHANGED]
19077
+ }),
19078
+ binary({
19079
+ id: "occupancy",
19080
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19081
+ role: DeviceRole.BinarySensor,
19082
+ outputCap: "binary",
19083
+ outputField: "on",
19084
+ defaultAggregate: "any",
19085
+ accept: [{
19086
+ cap: "motion",
19087
+ fieldPath: "detected"
19088
+ }, {
19089
+ cap: "binary",
19090
+ fieldPath: "on"
19091
+ }],
19092
+ companions: [CHANGED]
19093
+ }),
19094
+ binary({
19095
+ id: "binary",
19096
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19097
+ role: DeviceRole.BinarySensor,
19098
+ outputCap: "binary",
19099
+ outputField: "on",
19100
+ defaultAggregate: "any",
19101
+ accept: [{
19102
+ cap: "binary",
19103
+ fieldPath: "on"
19104
+ }],
19105
+ companions: [CHANGED]
19106
+ }),
19107
+ actuator({
19108
+ id: "switch",
19109
+ 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.",
19110
+ deviceType: DeviceType.Switch,
19111
+ outputCap: "switch",
19112
+ commandCaps: ["switch"]
19113
+ }),
19114
+ actuator({
19115
+ id: "light",
19116
+ 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.",
19117
+ deviceType: DeviceType.Light,
19118
+ outputCap: "switch",
19119
+ commandCaps: [
19120
+ "switch",
19121
+ "brightness",
19122
+ "color"
19123
+ ]
19124
+ })
19125
+ ];
19126
+ function actuator(input) {
19127
+ return {
19128
+ ...input,
19129
+ family: "actuator",
19130
+ role: void 0,
19131
+ memberDeviceType: input.deviceType,
19132
+ outputField: "on",
19133
+ quantity: "none",
19134
+ baseUnit: void 0,
19135
+ aggregates: BINARY_AGGREGATES,
19136
+ defaultAggregate: "any",
19137
+ empty: "offline",
19138
+ accept: [{
19139
+ cap: "switch",
19140
+ fieldPath: "on"
19141
+ }],
19142
+ companions: [CHANGED]
19143
+ };
19144
+ }
19145
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19146
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18759
19147
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18760
19148
  /**
18761
19149
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20582,12 +20970,12 @@ function buildDeviceAdminUrl(parts) {
20582
20970
  * providers detect renames by matching `uniqueId` and patch `id`
20583
20971
  * in-place so dispatch keeps working.
20584
20972
  *
20585
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20586
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20587
- * state push and are the single source of truth. The `device_class` tag is
20588
- * an HA-specific input that is consumed at the discovery boundary
20589
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20590
- * stored after that point.
20973
+ * **Display unit and precision are NOT stored here.** They are a read-time
20974
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20975
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20976
+ * neither is repeated on each sample. The `device_class` tag is an
20977
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20978
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20591
20979
  *
20592
20980
  * The `raw` escape hatch carries provider-specific attributes that don't
20593
20981
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20601,6 +20989,14 @@ var SourceInfoSchema = object({
20601
20989
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20602
20990
  * Used to detect rename when `id` changes. */
20603
20991
  uniqueId: string().optional(),
20992
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20993
+ quantity: ReadingQuantitySchema.optional(),
20994
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20995
+ dateTimeShape: _enum([
20996
+ "date",
20997
+ "time",
20998
+ "datetime"
20999
+ ]).optional(),
20604
21000
  /** Provider-specific extras that don't fit the structured slots.
20605
21001
  * Whitelist what goes here per provider — do NOT dump entire HA
20606
21002
  * attribute blobs (would explode the meta JSON column). */
@@ -41705,7 +42101,8 @@ method(object({
41705
42101
  "enum"
41706
42102
  ]),
41707
42103
  enumValues: array(string()).optional(),
41708
- item: boolean().optional()
42104
+ item: boolean().optional(),
42105
+ quantity: ReadingQuantitySchema.optional()
41709
42106
  })).readonly(),
41710
42107
  itemArray: object({
41711
42108
  path: string(),
@@ -45589,8 +45986,8 @@ var MaskPolygonShapeSchema = object({
45589
45986
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45590
45987
  var MaskGridShapeSchema = object({
45591
45988
  kind: literal("grid"),
45592
- gridWidth: number(),
45593
- gridHeight: number(),
45989
+ gridWidth: number().meta(readingMeta("none")),
45990
+ gridHeight: number().meta(readingMeta("none")),
45594
45991
  cells: array(boolean())
45595
45992
  });
45596
45993
  discriminatedUnion("kind", [
@@ -45702,6 +46099,8 @@ var NcSystemEventKindSchema = _enum([
45702
46099
  "detection-blind",
45703
46100
  "motion-noisy",
45704
46101
  "motion-calm",
46102
+ "package-llm-silent",
46103
+ "package-llm-answered",
45705
46104
  "recording-stalled",
45706
46105
  "recording-healthy",
45707
46106
  "recording-degraded",
@@ -57315,6 +57714,28 @@ var PackageVersionInfoSchema = object({
57315
57714
  deprecated: string().optional(),
57316
57715
  distTags: array(string()).readonly()
57317
57716
  });
57717
+ /**
57718
+ * One published document. `ok` is markdown from the registry. `missing` means
57719
+ * the registry answered and the file was not there. `error` is a registry or
57720
+ * tarball failure — never an empty string standing in for either.
57721
+ */
57722
+ var PackageDocBodySchema = discriminatedUnion("status", [
57723
+ object({
57724
+ status: literal("ok"),
57725
+ markdown: string()
57726
+ }),
57727
+ object({ status: literal("missing") }),
57728
+ object({
57729
+ status: literal("error"),
57730
+ message: string()
57731
+ })
57732
+ ]);
57733
+ var PackageDocsSchema = object({
57734
+ name: string(),
57735
+ version: string(),
57736
+ readme: PackageDocBodySchema,
57737
+ changelog: PackageDocBodySchema
57738
+ });
57318
57739
  var SearchResultSchema = object({
57319
57740
  name: string(),
57320
57741
  version: string(),
@@ -57533,7 +57954,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57533
57954
  }), object({ success: literal(true) }), {
57534
57955
  kind: "mutation",
57535
57956
  auth: "admin"
57536
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57957
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57958
+ name: string().min(1),
57959
+ version: string().min(1).optional()
57960
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57537
57961
  kind: "mutation",
57538
57962
  auth: "admin"
57539
57963
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57592,27 +58016,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57592
58016
  */
57593
58017
  var AirQualitySensorStatusSchema = object({
57594
58018
  /** Carbon dioxide concentration in ppm. */
57595
- co2Ppm: number().min(0).optional(),
58019
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57596
58020
  /** Total volatile organic compounds in ppb. */
57597
- vocPpb: number().min(0).optional(),
58021
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57598
58022
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57599
- pm25: number().min(0).optional(),
58023
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57600
58024
  /** Particulate matter ≤ 10 μm in µg/m³. */
57601
- pm10: number().min(0).optional(),
58025
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57602
58026
  /** Composite AQI value (typically 0..500). */
57603
- aqi: number().optional(),
58027
+ aqi: number().optional().meta(readingMeta("index")),
57604
58028
  /** Ms epoch when the slice was last updated. */
57605
- lastFetchedAt: number(),
57606
- /** Live display unit of the single metric this slice carries (e.g. HA
57607
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57608
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57609
- * per slice is unambiguous. */
57610
- unit: string().optional(),
57611
- /** Suggested decimal places for numeric display.
57612
- * Populated live from the upstream source when provided (e.g. HA
57613
- * `attributes.suggested_display_precision`). Falls back to
57614
- * auto-formatting when absent. */
57615
- precision: number().int().min(0).max(10).optional()
58029
+ lastFetchedAt: number()
57616
58030
  });
57617
58031
  var airQualitySensorCapability = {
57618
58032
  name: "air-quality-sensor",
@@ -57649,12 +58063,12 @@ var airQualitySensorCapability = {
57649
58063
  },
57650
58064
  pm25: {
57651
58065
  kind: "gauge",
57652
- unit: "ug/m3",
58066
+ unit: "µg/m³",
57653
58067
  deadband: 1
57654
58068
  },
57655
58069
  pm10: {
57656
58070
  kind: "gauge",
57657
- unit: "ug/m3",
58071
+ unit: "µg/m³",
57658
58072
  deadband: 1
57659
58073
  },
57660
58074
  aqi: {
@@ -57672,22 +58086,13 @@ var airQualitySensorCapability = {
57672
58086
  * entries with `device_class: illuminance`.
57673
58087
  */
57674
58088
  var AmbientLightSensorStatusSchema = object({
57675
- /** Current illuminance in lux (lx). */
57676
- lux: number().min(0).meta({
57677
- unit: "lx",
58089
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
58090
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
58091
+ precision: 0,
57678
58092
  label: "Illuminance"
57679
- }),
58093
+ })),
57680
58094
  /** Ms epoch when the slice was last updated. */
57681
- lastFetchedAt: number(),
57682
- /** Live display unit from the upstream source (e.g. HA
57683
- * `attributes.unit_of_measurement`). The UI prefers this over the
57684
- * role's canonical unit. Absent → fall back to the canonical unit. */
57685
- unit: string().optional(),
57686
- /** Suggested decimal places for numeric display.
57687
- * Populated live from the upstream source when provided (e.g. HA
57688
- * `attributes.suggested_display_precision`). Falls back to
57689
- * auto-formatting when absent. */
57690
- precision: number().int().min(0).max(10).optional()
58095
+ lastFetchedAt: number()
57691
58096
  });
57692
58097
  var ambientLightSensorCapability = {
57693
58098
  name: "ambient-light-sensor",
@@ -57719,12 +58124,7 @@ var ambientLightSensorCapability = {
57719
58124
  } } },
57720
58125
  /** Clock fields: written, but excluded from the compare that decides
57721
58126
  * whether persisting is worth a SQLite commit. */
57722
- volatileStateFields: ["lastFetchedAt"],
57723
- customize: {
57724
- version: 1,
57725
- advanced: "derive",
57726
- targets: []
57727
- }
58127
+ volatileStateFields: ["lastFetchedAt"]
57728
58128
  };
57729
58129
  /**
57730
58130
  * Per-class audio metrics aggregated over a sliding window.
@@ -57951,10 +58351,7 @@ var BatteryStatusSchema = object({
57951
58351
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57952
58352
  * declines to band a non-finite reading, which is the correct shape.
57953
58353
  */
57954
- percentage: number().min(0).max(100).nullable().meta({
57955
- unit: "%",
57956
- label: "Charge"
57957
- }),
58354
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57958
58355
  /**
57959
58356
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57960
58357
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57981,7 +58378,14 @@ var BatteryStatusSchema = object({
57981
58378
  */
57982
58379
  sleeping: boolean().meta({ label: "Sleeping" }),
57983
58380
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57984
- lastUpdated: number(),
58381
+ lastUpdated: number().meta(readingMeta("none")),
58382
+ /**
58383
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58384
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58385
+ * that only report charge. A second reading on this cap, not a second
58386
+ * `numeric-sensor` on the same device.
58387
+ */
58388
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57985
58389
  /**
57986
58390
  * Ms epoch of the last time the device PROVED it was reachable — a
57987
58391
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -58033,7 +58437,6 @@ var BATTERY_DEVICE_TYPES = [
58033
58437
  /** Generic Customize target for the percentage reading (D936). */
58034
58438
  var batteryCustomize = {
58035
58439
  version: 1,
58036
- advanced: "derive",
58037
58440
  targets: [{
58038
58441
  id: "percentage",
58039
58442
  cardinality: "scalar",
@@ -58042,7 +58445,7 @@ var batteryCustomize = {
58042
58445
  stateField: "percentage"
58043
58446
  },
58044
58447
  deviceTypes: BATTERY_DEVICE_TYPES,
58045
- apply: "add-or-replace",
58448
+ apply: "add",
58046
58449
  path: "percentage",
58047
58450
  sources: [{
58048
58451
  kind: "field",
@@ -58163,10 +58566,16 @@ onStatusChanged: { data: object({
58163
58566
  /**
58164
58567
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
58165
58568
  */
58166
- history: { fields: { percentage: {
58167
- kind: "gauge",
58168
- unit: "%"
58169
- } } },
58569
+ history: { fields: {
58570
+ percentage: {
58571
+ kind: "gauge",
58572
+ unit: "%"
58573
+ },
58574
+ volts: {
58575
+ kind: "gauge",
58576
+ unit: "V"
58577
+ }
58578
+ } },
58170
58579
  customize: batteryCustomize,
58171
58580
  /** Clock fields: written, but excluded from the compare that decides
58172
58581
  * whether persisting is worth a SQLite commit. */
@@ -58213,12 +58622,7 @@ var binaryCapability = {
58213
58622
  history: { fields: { on: { kind: "boolean" } } },
58214
58623
  /** Clock fields: written, but excluded from the compare that decides
58215
58624
  * whether persisting is worth a SQLite commit. */
58216
- volatileStateFields: ["lastChangedAt"],
58217
- customize: {
58218
- version: 1,
58219
- advanced: "derive",
58220
- targets: []
58221
- }
58625
+ volatileStateFields: ["lastChangedAt"]
58222
58626
  };
58223
58627
  /**
58224
58628
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -58234,7 +58638,7 @@ var binaryCapability = {
58234
58638
  */
58235
58639
  var BrightnessStatusSchema = object({
58236
58640
  /** Current level as 0..100 inclusive. Firmware-reported. */
58237
- percentage: number().min(0).max(100),
58641
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
58238
58642
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
58239
58643
  lastChangedAt: number()
58240
58644
  });
@@ -58688,21 +59092,21 @@ var ClimateControlStatusSchema = object({
58688
59092
  /** Single setpoint in Celsius. Used by single-target modes
58689
59093
  * (heat / cool / dry). Null when the device is in a range mode
58690
59094
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58691
- target: number().nullable(),
59095
+ target: number().nullable().meta(readingMeta("temperature")),
58692
59096
  /** Upper bound of the dual setpoint in Celsius. Populated only
58693
59097
  * in `heat_cool` mode. */
58694
- targetHigh: number().nullable(),
59098
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58695
59099
  /** Lower bound of the dual setpoint in Celsius. Populated only
58696
59100
  * in `heat_cool` mode. */
58697
- targetLow: number().nullable(),
59101
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58698
59102
  /** Target relative humidity (0..100). Null when the device has no
58699
59103
  * humidity control. */
58700
- targetHumidity: number().min(0).max(100).nullable(),
59104
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58701
59105
  /** Current measured temperature in Celsius. Read-only — pushed by
58702
59106
  * the device's internal sensor. */
58703
- currentTemp: number().nullable(),
59107
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58704
59108
  /** Current measured relative humidity (0..100). Read-only. */
58705
- currentHumidity: number().min(0).max(100).nullable(),
59109
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58706
59110
  /** Vertical louver swing on/off. Null when the device has no vertical
58707
59111
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58708
59112
  swingVertical: boolean().nullable(),
@@ -58710,11 +59114,11 @@ var ClimateControlStatusSchema = object({
58710
59114
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58711
59115
  swingHorizontal: boolean().nullable(),
58712
59116
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58713
- minTemp: number().nullable().optional(),
59117
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58714
59118
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58715
- maxTemp: number().nullable().optional(),
59119
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58716
59120
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58717
- tempStep: number().positive().nullable().optional(),
59121
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58718
59122
  /** Ms epoch when the slice was last updated (push or command). */
58719
59123
  lastFetchedAt: number()
58720
59124
  });
@@ -58856,17 +59260,17 @@ var climateControlCapability = {
58856
59260
  * modes to expose in the picker without a slice fetch.
58857
59261
  */
58858
59262
  var RgbTripletSchema = object({
58859
- r: number().int().min(0).max(255),
58860
- g: number().int().min(0).max(255),
58861
- b: number().int().min(0).max(255)
59263
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59264
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59265
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58862
59266
  });
58863
59267
  var HsvTripletSchema = object({
58864
59268
  /** Hue in degrees, 0..360. */
58865
- h: number().min(0).max(360),
59269
+ h: number().min(0).max(360).meta(readingMeta("none")),
58866
59270
  /** Saturation as 0..100 inclusive. */
58867
- s: number().min(0).max(100),
59271
+ s: number().min(0).max(100).meta(readingMeta("none")),
58868
59272
  /** Value/brightness as 0..100 inclusive. */
58869
- v: number().min(0).max(100)
59273
+ v: number().min(0).max(100).meta(readingMeta("none"))
58870
59274
  });
58871
59275
  var ColorInputSchema = discriminatedUnion("mode", [
58872
59276
  object({
@@ -58895,7 +59299,7 @@ var ColorStatusSchema = object({
58895
59299
  /** Populated when `mode === 'hsv'`. */
58896
59300
  hsv: HsvTripletSchema.optional(),
58897
59301
  /** Populated when `mode === 'mired'`. */
58898
- mireds: number().int().optional(),
59302
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58899
59303
  /** Ms epoch of the last operator-driven change. */
58900
59304
  lastChangedAt: number()
58901
59305
  });
@@ -59090,18 +59494,12 @@ _enum([
59090
59494
  "empty",
59091
59495
  "full"
59092
59496
  ]);
59093
- /** Units a remaining-life counter is reported in. Closed: the panel renders
59094
- * each. */
59095
- var ConsumableRemainingUnitSchema = _enum([
59096
- "days",
59097
- "hours",
59098
- "cycles"
59099
- ]);
59100
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
59101
- * days). `value` null = unknown (D393). */
59497
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59498
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59499
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
59102
59500
  var ConsumableRemainingSchema = object({
59103
- value: number().min(0).nullable(),
59104
- unit: ConsumableRemainingUnitSchema
59501
+ value: number().min(0).nullable().meta(readingMeta("device")),
59502
+ kind: ConsumableRemainingKindSchema
59105
59503
  });
59106
59504
  /** A single consumable item. Either a continuous `level` (remaining
59107
59505
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -59114,7 +59512,7 @@ var ConsumableItemSchema = object({
59114
59512
  /** Display name. */
59115
59513
  label: string().min(1),
59116
59514
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
59117
- level: number().min(0).max(100).nullable(),
59515
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59118
59516
  /** The provider's discrete verdict when known (binary mode). */
59119
59517
  status: ConsumableStatusSchema.nullable(),
59120
59518
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -59196,8 +59594,8 @@ var consumablesCustomize = {
59196
59594
  transforms: [{ kind: "identity" }]
59197
59595
  }],
59198
59596
  literals: [{
59199
- path: "remaining.unit",
59200
- values: [...ConsumableRemainingUnitSchema.options]
59597
+ path: "remaining.kind",
59598
+ values: [...ConsumableRemainingKindSchema.options]
59201
59599
  }]
59202
59600
  },
59203
59601
  {
@@ -59326,7 +59724,6 @@ reset: method(object({
59326
59724
  },
59327
59725
  "remaining.value": {
59328
59726
  kind: "gauge",
59329
- unitFrom: "remaining.unit",
59330
59727
  deadband: 1,
59331
59728
  displayName: "Remaining"
59332
59729
  }
@@ -59396,10 +59793,9 @@ var contactCapability = {
59396
59793
  * (empty array for the other kinds). It lives in the slice because
59397
59794
  * HA `select.*` entities may update their options at runtime.
59398
59795
  *
59399
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59400
- * `kind === 'numeric'` and are absent (undefined) for all other
59401
- * kinds. They are populated live from HA attributes on each state
59402
- * push — consistent with `options` and the sensor-unit approach.
59796
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59797
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59798
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59403
59799
  */
59404
59800
  var ControlStatusSchema = object({
59405
59801
  kind: _enum([
@@ -59411,39 +59807,17 @@ var ControlStatusSchema = object({
59411
59807
  /** Current value. Numeric kind → number; select/text/datetime → string.
59412
59808
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59413
59809
  * or time-only — HA accepts all three forms). */
59414
- value: union([number(), string()]),
59810
+ value: union([number().meta(readingMeta("device")), string()]),
59415
59811
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59416
59812
  options: array(string()),
59417
59813
  /** Ms epoch when the slice was last updated. */
59418
59814
  lastChangedAt: number(),
59419
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59420
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59421
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59422
- unit: string().optional(),
59423
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59424
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59425
- min: number().optional(),
59426
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59427
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59428
- max: number().optional(),
59429
- /** Step increment. Populated live from HA `attributes.step`.
59430
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59431
- step: number().optional(),
59432
- /** Suggested decimal places for numeric display. Populated live from HA
59433
- * `attributes.suggested_display_precision`, or derived from `step` when the
59434
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59435
- * other kinds. Falls back to auto-formatting when absent. */
59436
- precision: number().int().min(0).max(10).optional(),
59437
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59438
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59439
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59440
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59441
- * `has_date`/`has_time` attributes on each push. */
59442
- format: _enum([
59443
- "date",
59444
- "time",
59445
- "datetime"
59446
- ]).optional()
59815
+ /** Minimum allowed value, in the device quantity's base unit. */
59816
+ min: number().optional().meta(readingMeta("device")),
59817
+ /** Maximum allowed value, in the device quantity's base unit. */
59818
+ max: number().optional().meta(readingMeta("device")),
59819
+ /** Step increment, in the device quantity's base unit. */
59820
+ step: number().optional().meta(readingMeta("device"))
59447
59821
  });
59448
59822
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59449
59823
  object({
@@ -59509,9 +59883,9 @@ var CoverStatusSchema = object({
59509
59883
  ]),
59510
59884
  /** 0 = fully closed, 100 = fully open. Null when the device has
59511
59885
  * no intermediate position surface. */
59512
- position: number().min(0).max(100).nullable(),
59886
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59513
59887
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59514
- tiltPosition: number().min(0).max(100).nullable(),
59888
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59515
59889
  /** Ms epoch when the slice was last updated. */
59516
59890
  lastChangedAt: number()
59517
59891
  });
@@ -59614,9 +59988,9 @@ var NormalizedRangeSchema$1 = object({
59614
59988
  var DayNightStatusSchema = object({
59615
59989
  mode: DayNightModeSchema,
59616
59990
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59617
- sensitivity: number().optional(),
59991
+ sensitivity: number().optional().meta(readingMeta("none")),
59618
59992
  /** Delay before the IR-cut filter flips, in seconds. */
59619
- switchDelaySec: number().optional(),
59993
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59620
59994
  lastFetchedAt: number()
59621
59995
  });
59622
59996
  /**
@@ -59834,11 +60208,11 @@ var DishwasherStatusSchema = object({
59834
60208
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59835
60209
  availablePrograms: array(string()),
59836
60210
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59837
- remainingSeconds: number().int().nonnegative().nullable(),
60211
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59838
60212
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59839
- delayedStartSeconds: number().int().nonnegative().nullable(),
60213
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59840
60214
  /** Cycle completion 0..100, or null when not applicable. */
59841
- progressPercent: number().min(0).max(100).nullable(),
60215
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59842
60216
  /** True when the door is reported open; null when the door state is not known. */
59843
60217
  doorOpen: boolean().nullable(),
59844
60218
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59862,7 +60236,7 @@ var DishwasherStatusSchema = object({
59862
60236
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59863
60237
  machineCareDue: boolean().nullable(),
59864
60238
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59865
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
60239
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59866
60240
  /** Option states: null when the appliance does not expose the option or has not said. */
59867
60241
  halfLoad: boolean().nullable(),
59868
60242
  hygienePlus: boolean().nullable(),
@@ -59877,7 +60251,7 @@ var DishwasherStatusSchema = object({
59877
60251
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59878
60252
  activeEvents: array(string()),
59879
60253
  /** Numeric error code when the appliance reports one; null otherwise. */
59880
- errorCode: number().nullable(),
60254
+ errorCode: number().nullable().meta(readingMeta("none")),
59881
60255
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59882
60256
  errorLabel: string().nullable(),
59883
60257
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -60108,7 +60482,7 @@ var DoorbellStatusSchema = object({
60108
60482
  /** Ms epoch of the last press. null = never observed since this provider started. */
60109
60483
  lastPressedAt: number().nullable(),
60110
60484
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
60111
- pressCountSinceStart: number()
60485
+ pressCountSinceStart: number().meta(readingMeta("count"))
60112
60486
  });
60113
60487
  var DoorbellPressEventSchema = object({
60114
60488
  deviceId: number(),
@@ -60173,17 +60547,7 @@ onPressed: {
60173
60547
  history: "none",
60174
60548
  customize: doorbellCustomize
60175
60549
  };
60176
- /**
60177
- * Enum-state sensor — a string value picked from a finite option set.
60178
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
60179
- * states, weather conditions, contact-source channels, mode strings).
60180
- * The option set is stable across observations; it is persisted in the
60181
- * device config blob (via the control slice at adoption) and is NOT
60182
- * stored in `sourceInfo`.
60183
- */
60184
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
60185
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
60186
- var EnumSensorDateTimeFormatSchema = _enum([
60550
+ _enum([
60187
60551
  "date",
60188
60552
  "time",
60189
60553
  "datetime"
@@ -60191,12 +60555,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
60191
60555
  var EnumSensorStatusSchema = object({
60192
60556
  value: string(),
60193
60557
  /**
60194
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
60195
- * as a locale date/time/datetime; absent for plain enum sensors. HA
60196
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
60197
- * a time-only sensor → `'time'`.
60558
+ * Set when `value` is a timestamp rather than an enum state, so history
60559
+ * does not learn ISO strings as states. The display shape (date / time /
60560
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
60198
60561
  */
60199
- format: EnumSensorDateTimeFormatSchema.optional(),
60562
+ timestamp: boolean().optional(),
60200
60563
  /** Ms epoch when the slice was last updated. */
60201
60564
  lastFetchedAt: number()
60202
60565
  });
@@ -60223,11 +60586,11 @@ var enumSensorCapability = {
60223
60586
  * History (D850): the value as an `enum` series (an appliance's program or
60224
60587
  * operation state, an HVAC action). The option set is per device, so the
60225
60588
  * value list is LEARNED as values are first seen (bounded). A sensor with a
60226
- * `format` carries a timestamp, not a state, and is not recorded.
60589
+ * `timestamp` carries a clock, not a state, and is not recorded.
60227
60590
  */
60228
60591
  history: { fields: { value: {
60229
60592
  kind: "enum",
60230
- skipWhenSet: "format"
60593
+ skipWhenSet: "timestamp"
60231
60594
  } } },
60232
60595
  /** Clock fields: written, but excluded from the compare that decides
60233
60596
  * whether persisting is worth a SQLite commit. */
@@ -60247,13 +60610,13 @@ var EventFireSchema = object({
60247
60610
  deviceId: number(),
60248
60611
  eventType: string(),
60249
60612
  data: record(string(), unknown()).nullable(),
60250
- timestamp: number(),
60251
- seq: number()
60613
+ timestamp: number().meta(readingMeta("none")),
60614
+ seq: number().meta(readingMeta("count"))
60252
60615
  });
60253
60616
  var EventEmitterStatusSchema = object({
60254
60617
  eventTypes: array(string()),
60255
60618
  lastEvent: EventFireSchema.nullable(),
60256
- eventCountSinceStart: number()
60619
+ eventCountSinceStart: number().meta(readingMeta("count"))
60257
60620
  });
60258
60621
  var eventEmitterCapability = {
60259
60622
  name: "event-emitter",
@@ -60666,10 +61029,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60666
61029
  var FanControlStatusSchema = object({
60667
61030
  /** Active speed as 0..100 inclusive. Null when the device has no
60668
61031
  * speed surface (single-speed fan). */
60669
- percentage: number().min(0).max(100).nullable(),
61032
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60670
61033
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60671
61034
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60672
- percentageStep: number().positive().optional(),
61035
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60673
61036
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60674
61037
  * — empty string when no preset is active or supported. */
60675
61038
  preset: string(),
@@ -60790,7 +61153,7 @@ var FeatureProbeStatusSchema = object({
60790
61153
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60791
61154
  model: string().nullable(),
60792
61155
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60793
- channelCount: number().nullable(),
61156
+ channelCount: number().nullable().meta(readingMeta("count")),
60794
61157
  /**
60795
61158
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60796
61159
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60913,9 +61276,91 @@ var gasCapability = {
60913
61276
  * whether persisting is worth a SQLite commit. */
60914
61277
  volatileStateFields: ["lastChangedAt"]
60915
61278
  };
61279
+ var GroupCommandsSchema = object({
61280
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61281
+ results: array(object({
61282
+ deviceId: number().int().nonnegative(),
61283
+ cap: string().min(1),
61284
+ method: string().min(1),
61285
+ ok: boolean(),
61286
+ error: string().optional(),
61287
+ /** Ms epoch of the attempt. */
61288
+ at: number()
61289
+ })) });
61290
+ var groupCommandsCapability = {
61291
+ name: "group-commands",
61292
+ scope: "device",
61293
+ deviceNative: true,
61294
+ mode: "singleton",
61295
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
61296
+ methods: {},
61297
+ status: {
61298
+ schema: GroupCommandsSchema,
61299
+ kind: "push"
61300
+ },
61301
+ runtimeState: GroupCommandsSchema,
61302
+ /**
61303
+ * Runtime-state durability: **session** — the last attempt. A restored
61304
+ * failure would outlive the command that caused it.
61305
+ *
61306
+ * See `RuntimeStateDurability`. Enforced by
61307
+ * `scripts/check-runtime-state-durability.ts`.
61308
+ */
61309
+ durability: "session",
61310
+ history: "none"
61311
+ };
61312
+ /**
61313
+ * How many members of a helper group answered (D944). Counts are integers,
61314
+ * not measurements: zero included is a real count, never a stand-in reading.
61315
+ * The reading itself stays on the output capability. History is none — the
61316
+ * series that later statistics read is the output cap's own history block.
61317
+ */
61318
+ var GroupStatusSchema = object({
61319
+ /** Members whose current value entered the aggregate. */
61320
+ included: number().int().nonnegative().meta(readingMeta("count", {
61321
+ precision: 0,
61322
+ label: "Reporting"
61323
+ })),
61324
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61325
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61326
+ precision: 0,
61327
+ label: "Excluded"
61328
+ })),
61329
+ /** `included` + `excluded`. */
61330
+ total: number().int().nonnegative().meta(readingMeta("count", {
61331
+ precision: 0,
61332
+ label: "Members"
61333
+ }))
61334
+ });
61335
+ var groupStatusCapability = {
61336
+ name: "group-status",
61337
+ scope: "device",
61338
+ deviceNative: true,
61339
+ mode: "singleton",
61340
+ deviceTypes: [
61341
+ DeviceType.Sensor,
61342
+ DeviceType.Switch,
61343
+ DeviceType.Light
61344
+ ],
61345
+ methods: {},
61346
+ status: {
61347
+ schema: GroupStatusSchema,
61348
+ kind: "push"
61349
+ },
61350
+ runtimeState: GroupStatusSchema,
61351
+ /**
61352
+ * Runtime-state durability: **session** — recomputed from members on the
61353
+ * next source event; a restored count would outlive the readings it named.
61354
+ *
61355
+ * See `RuntimeStateDurability`. Enforced by
61356
+ * `scripts/check-runtime-state-durability.ts`.
61357
+ */
61358
+ durability: "session",
61359
+ history: "none"
61360
+ };
60916
61361
  var HeatingValveStatusSchema = object({
60917
61362
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60918
- position: number().min(0).max(100).nullable(),
61363
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60919
61364
  /** Ms epoch when the slice was last updated (push or poll). */
60920
61365
  lastFetchedAt: number()
60921
61366
  });
@@ -60958,9 +61403,9 @@ var HumidifierStatusSchema = object({
60958
61403
  /** Whether the humidifier is currently on. */
60959
61404
  on: boolean(),
60960
61405
  /** Current measured relative humidity (0..100). Null when not reported. */
60961
- currentHumidity: number().min(0).max(100).nullable(),
61406
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60962
61407
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60963
- targetHumidity: number().min(0).max(100).nullable(),
61408
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60964
61409
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60965
61410
  * device has no mode surface. */
60966
61411
  mode: string().nullable(),
@@ -60970,9 +61415,9 @@ var HumidifierStatusSchema = object({
60970
61415
  * `idle` / `off`). Null when the device doesn't report it. */
60971
61416
  action: string().nullable(),
60972
61417
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60973
- minHumidity: number().nullable(),
61418
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60974
61419
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60975
- maxHumidity: number().nullable(),
61420
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60976
61421
  /** Ms epoch when the slice was last updated. */
60977
61422
  lastChangedAt: number()
60978
61423
  });
@@ -61042,22 +61487,13 @@ var humidifierCapability = {
61042
61487
  * `sourceInfo`.
61043
61488
  */
61044
61489
  var HumiditySensorStatusSchema = object({
61045
- /** Current relative humidity, 0..100. */
61046
- percent: number().min(0).max(100).meta({
61047
- unit: "%",
61490
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61491
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61492
+ precision: 0,
61048
61493
  label: "Humidity"
61049
- }),
61494
+ })),
61050
61495
  /** Ms epoch when the slice was last updated. */
61051
- lastFetchedAt: number(),
61052
- /** Live display unit from the upstream source (e.g. HA
61053
- * `attributes.unit_of_measurement`). The UI prefers this over the
61054
- * role's canonical unit. Absent → fall back to the canonical unit. */
61055
- unit: string().optional(),
61056
- /** Suggested decimal places for numeric display.
61057
- * Populated live from the upstream source when provided (e.g. HA
61058
- * `attributes.suggested_display_precision`). Falls back to
61059
- * auto-formatting when absent. */
61060
- precision: number().int().min(0).max(10).optional()
61496
+ lastFetchedAt: number()
61061
61497
  });
61062
61498
  var humiditySensorCapability = {
61063
61499
  name: "humidity-sensor",
@@ -61088,12 +61524,7 @@ var humiditySensorCapability = {
61088
61524
  } } },
61089
61525
  /** Clock fields: written, but excluded from the compare that decides
61090
61526
  * whether persisting is worth a SQLite commit. */
61091
- volatileStateFields: ["lastFetchedAt"],
61092
- customize: {
61093
- version: 1,
61094
- advanced: "derive",
61095
- targets: []
61096
- }
61527
+ volatileStateFields: ["lastFetchedAt"]
61097
61528
  };
61098
61529
  /**
61099
61530
  * Image display cap. Models a single still image exposed by an integration —
@@ -61114,7 +61545,7 @@ var ImageStatusSchema = object({
61114
61545
  * entity exposes no `entity_picture` (yet). */
61115
61546
  url: string().nullable(),
61116
61547
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
61117
- lastUpdated: number().nullable()
61548
+ lastUpdated: number().nullable().meta(readingMeta("none"))
61118
61549
  });
61119
61550
  var imageCapability = {
61120
61551
  name: "image",
@@ -61202,19 +61633,19 @@ var NormalizedRangeSchema = object({
61202
61633
  */
61203
61634
  var ImageSettingsStatusSchema = object({
61204
61635
  /** Normalized 0–100. */
61205
- brightness: number().optional(),
61636
+ brightness: number().optional().meta(readingMeta("none")),
61206
61637
  /** Normalized 0–100. */
61207
- contrast: number().optional(),
61638
+ contrast: number().optional().meta(readingMeta("none")),
61208
61639
  /** Normalized 0–100. */
61209
- saturation: number().optional(),
61640
+ saturation: number().optional().meta(readingMeta("none")),
61210
61641
  /** Normalized 0–100. */
61211
- sharpness: number().optional(),
61642
+ sharpness: number().optional().meta(readingMeta("none")),
61212
61643
  mirror: boolean().optional(),
61213
61644
  flip: boolean().optional(),
61214
61645
  rotate: ImageRotateSchema.optional(),
61215
61646
  whiteBalance: WhiteBalanceModeSchema.optional(),
61216
61647
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
61217
- warmth: number().optional(),
61648
+ warmth: number().optional().meta(readingMeta("none")),
61218
61649
  exposureMode: ExposureModeSchema.optional(),
61219
61650
  backlightMode: BacklightModeSchema.optional(),
61220
61651
  lastFetchedAt: number()
@@ -61493,10 +61924,10 @@ var IntercomAbilitySchema = object({
61493
61924
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61494
61925
  codecs: array(string()),
61495
61926
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61496
- sampleRate: number(),
61927
+ sampleRate: number().meta(readingMeta("none")),
61497
61928
  duplex: _enum(["half", "full"]),
61498
61929
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61499
- maxBacklogMs: number()
61930
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61500
61931
  });
61501
61932
  var IntercomStatusSchema = object({
61502
61933
  /** True while a talk session is open. */
@@ -61723,13 +62154,13 @@ var LawnMowerControlStatusSchema = object({
61723
62154
  activity: LawnMowerActivitySchema,
61724
62155
  /** 0..100 battery percentage. Null when the device has no battery
61725
62156
  * reading. */
61726
- batteryLevel: number().min(0).max(100).nullable(),
62157
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61727
62158
  /** 0..100 mowing-completion percentage of the current task, or null when no
61728
62159
  * task is active / progress is unavailable. */
61729
- progressPercent: number().min(0).max(100).nullable(),
62160
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61730
62161
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61731
62162
  * or null when unknown. */
61732
- currentCode: number().nullable(),
62163
+ currentCode: number().nullable().meta(readingMeta("none")),
61733
62164
  /** Human label for {@link currentCode}, or null when undecodable. */
61734
62165
  currentCodeLabel: string().nullable(),
61735
62166
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62262,7 +62693,7 @@ var MediaPlayerStatusSchema = object({
62262
62693
  state: MediaPlayerStateSchema,
62263
62694
  /** Volume as 0..100 inclusive. Null when the device has no volume
62264
62695
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62265
- volumeLevel: number().min(0).max(100).nullable(),
62696
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62266
62697
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62267
62698
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62268
62699
  isMuted: boolean().nullable(),
@@ -62275,10 +62706,10 @@ var MediaPlayerStatusSchema = object({
62275
62706
  currentMedia: MediaInfoSchema.nullable(),
62276
62707
  /** Current playback position in ms. Null when not seekable or
62277
62708
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62278
- positionMs: number().int().nonnegative().nullable(),
62709
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62279
62710
  /** Total duration of the current media in ms. Null when unknown
62280
62711
  * (live stream) or nothing is playing. */
62281
- durationMs: number().int().nonnegative().nullable(),
62712
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62282
62713
  /** Shuffle toggle. Null when no shuffle surface.
62283
62714
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62284
62715
  shuffle: boolean().nullable(),
@@ -62587,7 +63018,7 @@ var motionCustomize = {
62587
63018
  eventName: "onMotionChanged"
62588
63019
  },
62589
63020
  deviceTypes: [DeviceType.Camera],
62590
- apply: "replace",
63021
+ apply: "add",
62591
63022
  applyAuthority: {
62592
63023
  kind: "camera-sensors",
62593
63024
  timeoutField: "motionCooldownMs"
@@ -62659,7 +63090,7 @@ var MotionStatusSchema = object({
62659
63090
  * it, every provider seeded it at boot, and every session left it
62660
63091
  * `detected: true` after its detach.
62661
63092
  */
62662
- autoClearAfterMs: number().nullable()
63093
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62663
63094
  });
62664
63095
  /**
62665
63096
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62696,12 +63127,18 @@ var motionCapability = {
62696
63127
  methods: {
62697
63128
  /**
62698
63129
  * Pull the current motion state synchronously. Convenience shortcut
62699
- * for consumers that don't need the full status object; equivalent
62700
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62701
- * `getStatus` once the auto-injected status surface lands in every
62702
- * consumer.
63130
+ * for consumers that don't need the full status object. A composed
63131
+ * group answers from its slice (D944): unknown throws, it is not off.
62703
63132
  */
62704
63133
  isDetected: method(object({ deviceId: number() }), boolean()) },
63134
+ /**
63135
+ * A composed motion group serves `isDetected` from `detected` (D944).
63136
+ * Without this the cap stays uncomposable (`has-commands`).
63137
+ */
63138
+ composedMethods: { isDetected: {
63139
+ kind: "slice",
63140
+ field: "detected"
63141
+ } },
62705
63142
  events: {
62706
63143
  /**
62707
63144
  * Fires every time the runner transitions a camera between
@@ -62841,7 +63278,7 @@ var MotionZoneRegionSchema = object({
62841
63278
  * the grid region(s). */
62842
63279
  var MotionZoneStatusSchema = object({
62843
63280
  enabled: boolean(),
62844
- sensitivity: number(),
63281
+ sensitivity: number().meta(readingMeta("none")),
62845
63282
  /** Grid region(s). Today exactly one `grid` shape. */
62846
63283
  regions: array(MotionZoneRegionSchema),
62847
63284
  lastFetchedAt: number()
@@ -63211,7 +63648,7 @@ var NavigationStatusSchema = object({
63211
63648
  following: boolean(),
63212
63649
  flash: boolean(),
63213
63650
  lightMode: NavigationLightModeSchema,
63214
- lightLevel: number().min(40).max(100),
63651
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
63215
63652
  /** Ms epoch when the slice was last updated. */
63216
63653
  lastChangedAt: number()
63217
63654
  });
@@ -63345,13 +63782,13 @@ var NetworkLinkStatusSchema = object({
63345
63782
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63346
63783
  * SKIP a null rather than coerce it.
63347
63784
  */
63348
- signalPercent: number().min(0).max(100).nullable(),
63785
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63349
63786
  /** Raw received signal strength in dBm, when the firmware reports one. */
63350
- rssiDbm: number().optional(),
63787
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63351
63788
  /** Network name of a wireless link, when the firmware reports it. */
63352
63789
  ssid: string().optional(),
63353
63790
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63354
- lastUpdated: number()
63791
+ lastUpdated: number().meta(readingMeta("none"))
63355
63792
  });
63356
63793
  /** The slice a provider seeds before its first read: nothing is known yet. */
63357
63794
  var NETWORK_LINK_UNKNOWN = {
@@ -63756,7 +64193,7 @@ var NotifierStatusSchema = object({
63756
64193
  lastError: string().nullable(),
63757
64194
  /** Number of deliveries currently buffered server-side (rate-limited
63758
64195
  * notifiers may queue). 0 when the provider sends synchronously. */
63759
- queueDepth: number().int().nonnegative(),
64196
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63760
64197
  /** Per-feature capability matrix — authoritative source for the
63761
64198
  * compose-form field gating. */
63762
64199
  supports: NotifierSupportsSchema
@@ -63848,11 +64285,9 @@ onSent: { data: object({
63848
64285
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63849
64286
  * for: water flow, distance, weight, frequency, signal strength, …).
63850
64287
  *
63851
- * The `unit` and `precision` fields carry the live display metadata that
63852
- * the upstream source provides with each state push (HA
63853
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63854
- * The UI reads them directly from the slice — they are the single source
63855
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64288
+ * The quantity is not on this slice. It is chosen when the device is
64289
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64290
+ * that quantity's base unit. `null` is a refused sample (D393).
63856
64291
  *
63857
64292
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63858
64293
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63861,15 +64296,8 @@ onSent: { data: object({
63861
64296
  * numeric values land here.
63862
64297
  */
63863
64298
  var NumericSensorStatusSchema = object({
63864
- value: number(),
63865
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63866
- * Populated live from the upstream source on each state push.
63867
- * Absent for unitless sensors. */
63868
- unit: string().optional(),
63869
- /** Suggested decimal places for numeric display.
63870
- * Populated live from the upstream source when provided.
63871
- * Falls back to auto-formatting when absent. */
63872
- precision: number().int().min(0).max(10).optional(),
64299
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64300
+ value: number().nullable().meta(readingMeta("device")),
63873
64301
  /** Ms epoch when the slice was last updated. */
63874
64302
  lastFetchedAt: number()
63875
64303
  });
@@ -63893,11 +64321,11 @@ var numericSensorCapability = {
63893
64321
  */
63894
64322
  durability: "restored",
63895
64323
  /**
63896
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64324
+ * History (D850): recorded series. The unit is the device quantity's base,
64325
+ * resolved when the series is read — the cap has no single unit.
63897
64326
  */
63898
64327
  history: { fields: { value: {
63899
64328
  kind: "gauge",
63900
- unitFrom: "unit",
63901
64329
  minIntervalMs: 3e4
63902
64330
  } } },
63903
64331
  /** Clock fields: written, but excluded from the compare that decides
@@ -64235,23 +64663,26 @@ var PetFeederStatusSchema = object({
64235
64663
  /** Food currently in the bowl (grams). Null when the device has not
64236
64664
  * reported a reading yet. On dual-hopper models this is the combined
64237
64665
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
64238
- foodLevel: number().nullable(),
64666
+ foodLevel: number().nullable().meta(readingMeta("none")),
64239
64667
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
64240
64668
  * single-hopper models. */
64241
- food1: number().nullable(),
64669
+ food1: number().nullable().meta(readingMeta("none")),
64242
64670
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
64243
64671
  * single-hopper models. */
64244
- food2: number().nullable(),
64672
+ food2: number().nullable().meta(readingMeta("none")),
64245
64673
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
64246
64674
  * (`device_class: problem`, on = low). True when the bowl is empty /
64247
64675
  * below the feeder's low threshold. */
64248
64676
  lowFood: boolean(),
64249
64677
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
64250
64678
  * device has no battery reading. */
64251
- batteryPower: number().min(0).max(100).nullable(),
64252
- /** Days of desiccant life remaining. Null when the model has no
64253
- * desiccant sensor. */
64254
- desiccantLeftDays: number().nullable(),
64679
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64680
+ /**
64681
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64682
+ * Providers convert from the days the feeder reports. Null when the
64683
+ * model has no desiccant sensor.
64684
+ */
64685
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
64255
64686
  /** True while a feed is in progress. */
64256
64687
  feeding: boolean(),
64257
64688
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -64262,7 +64693,7 @@ var PetFeederStatusSchema = object({
64262
64693
  * with `errorCode` for consumers that want the raw integer. */
64263
64694
  error: string().nullable(),
64264
64695
  /** Raw device error code (0 / null = no error). */
64265
- errorCode: number().nullable(),
64696
+ errorCode: number().nullable().meta(readingMeta("none")),
64266
64697
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
64267
64698
  isDualHopper: boolean(),
64268
64699
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -64272,7 +64703,7 @@ var PetFeederStatusSchema = object({
64272
64703
  /** Play a chime when dispensing. */
64273
64704
  feedSound: boolean(),
64274
64705
  /** Speaker / prompt volume level (device-scaled integer). */
64275
- volume: number(),
64706
+ volume: number().meta(readingMeta("none")),
64276
64707
  /** Ms epoch when the slice was last refreshed from the cloud. */
64277
64708
  lastFetchedAt: number()
64278
64709
  });
@@ -64391,7 +64822,7 @@ var petFeederCapability = {
64391
64822
  },
64392
64823
  desiccantLeftDays: {
64393
64824
  kind: "gauge",
64394
- unit: "days"
64825
+ unit: "s"
64395
64826
  }
64396
64827
  } }
64397
64828
  };
@@ -64792,36 +65223,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64792
65223
  */
64793
65224
  var PowerMeterStatusSchema = object({
64794
65225
  /** Instantaneous power draw in watts. */
64795
- watts: number().optional().meta({
64796
- unit: "W",
64797
- label: "Power"
64798
- }),
65226
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64799
65227
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64800
- kwhTotal: number().optional().meta({
64801
- unit: "kWh",
64802
- label: "Energy"
64803
- }),
65228
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64804
65229
  /** Voltage in volts. */
64805
- volts: number().optional(),
65230
+ volts: number().optional().meta(readingMeta("voltage")),
64806
65231
  /** Current in amperes. */
64807
- amps: number().optional(),
64808
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64809
- va: number().optional(),
65232
+ amps: number().optional().meta(readingMeta("current")),
65233
+ /** Apparent power in volt-amperes. */
65234
+ va: number().optional().meta(readingMeta("apparent-power")),
64810
65235
  /** Power factor (0..1) if reported. */
64811
- powerFactor: number().min(0).max(1).optional(),
65236
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64812
65237
  /** Ms epoch when the slice was last updated. */
64813
- lastFetchedAt: number(),
64814
- /** Live display unit of the single metric this slice carries (e.g. HA
64815
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64816
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64817
- * unit per slice is unambiguous. The UI prefers this over the role's
64818
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64819
- unit: string().optional(),
64820
- /** Suggested decimal places for numeric display.
64821
- * Populated live from the upstream source when provided (e.g. HA
64822
- * `attributes.suggested_display_precision`). Falls back to
64823
- * auto-formatting when absent. */
64824
- precision: number().int().min(0).max(10).optional()
65238
+ lastFetchedAt: number()
64825
65239
  });
64826
65240
  var powerMeterCapability = {
64827
65241
  name: "power-meter",
@@ -64885,12 +65299,7 @@ var powerMeterCapability = {
64885
65299
  } },
64886
65300
  /** Clock fields: written, but excluded from the compare that decides
64887
65301
  * whether persisting is worth a SQLite commit. */
64888
- volatileStateFields: ["lastFetchedAt"],
64889
- customize: {
64890
- version: 1,
64891
- advanced: "derive",
64892
- targets: []
64893
- }
65302
+ volatileStateFields: ["lastFetchedAt"]
64894
65303
  };
64895
65304
  /**
64896
65305
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64910,11 +65319,11 @@ var powerMeterCapability = {
64910
65319
  */
64911
65320
  var GpsLocationSchema = object({
64912
65321
  /** Latitude in decimal degrees, -90..90. */
64913
- latitude: number().min(-90).max(90),
65322
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64914
65323
  /** Longitude in decimal degrees, -180..180. */
64915
- longitude: number().min(-180).max(180),
65324
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64916
65325
  /** Reported accuracy in meters (lower = better). */
64917
- accuracyMeters: number().nonnegative()
65326
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64918
65327
  });
64919
65328
  var PresenceStatusSchema = object({
64920
65329
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64926,7 +65335,7 @@ var PresenceStatusSchema = object({
64926
65335
  gps: GpsLocationSchema.nullable(),
64927
65336
  /** Optional battery level of the tracking device (0..100). Null
64928
65337
  * when not reported. */
64929
- batteryPercent: number().min(0).max(100).nullable(),
65338
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64930
65339
  /** Ms epoch when the slice was last updated. */
64931
65340
  lastChangedAt: number()
64932
65341
  });
@@ -64976,19 +65385,10 @@ var presenceCapability = {
64976
65385
  * `sourceInfo`.
64977
65386
  */
64978
65387
  var PressureSensorStatusSchema = object({
64979
- /** Current pressure in hPa. */
64980
- hpa: number(),
65388
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65389
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64981
65390
  /** Ms epoch when the slice was last updated. */
64982
- lastFetchedAt: number(),
64983
- /** Live display unit from the upstream source (e.g. HA
64984
- * `attributes.unit_of_measurement`). The UI prefers this over the
64985
- * role's canonical unit. Absent → fall back to the canonical unit. */
64986
- unit: string().optional(),
64987
- /** Suggested decimal places for numeric display.
64988
- * Populated live from the upstream source when provided (e.g. HA
64989
- * `attributes.suggested_display_precision`). Falls back to
64990
- * auto-formatting when absent. */
64991
- precision: number().int().min(0).max(10).optional()
65391
+ lastFetchedAt: number()
64992
65392
  });
64993
65393
  var pressureSensorCapability = {
64994
65394
  name: "pressure-sensor",
@@ -65514,8 +65914,8 @@ var ptzCapability = {
65514
65914
  };
65515
65915
  var PtzAutotrackSettingsSchema = object({
65516
65916
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65517
- stopDelaySeconds: number().int().min(0).max(300),
65518
- disappearDelaySeconds: number().int().min(0).max(300)
65917
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65918
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65519
65919
  });
65520
65920
  /**
65521
65921
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65669,7 +66069,7 @@ var RunningStateStatusSchema = object({
65669
66069
  /** null = unknown — never coerced to false (D393). */
65670
66070
  running: boolean().nullable(),
65671
66071
  /** Epoch ms the current boolean began; null while `running` is null. */
65672
- since: number().nullable(),
66072
+ since: number().nullable().meta(readingMeta("none")),
65673
66073
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65674
66074
  phase: string().nullable(),
65675
66075
  /** null until a reading has been published (D393). */
@@ -65684,7 +66084,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65684
66084
  /** Generic Customize target for the running boolean (D936). */
65685
66085
  var runningCustomize = {
65686
66086
  version: 1,
65687
- advanced: "derive",
65688
66087
  targets: [{
65689
66088
  id: "running",
65690
66089
  cardinality: "scalar",
@@ -65704,7 +66103,7 @@ var runningCustomize = {
65704
66103
  DeviceType.Thermostat,
65705
66104
  DeviceType.PetFeeder
65706
66105
  ],
65707
- apply: "add-or-replace",
66106
+ apply: "add",
65708
66107
  path: "running",
65709
66108
  sources: [
65710
66109
  {
@@ -66997,9 +67396,9 @@ var RecordTriggerSchema = _enum([
66997
67396
  */
66998
67397
  var RecordWindowSchema = object({
66999
67398
  trigger: RecordTriggerSchema,
67000
- day: number().int().min(0).max(6),
67001
- startMinute: number().int().min(0).max(1439),
67002
- endMinute: number().int().min(1).max(1440)
67399
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67400
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67401
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
67003
67402
  });
67004
67403
  /** Status of one physical volume, as the camera itself describes it. */
67005
67404
  var OnboardStorageVolumeSchema = object({
@@ -67075,10 +67474,10 @@ var RecordingOnboardStatusSchema = object({
67075
67474
  /** Master "record to the card at all" switch. */
67076
67475
  enabled: boolean().nullable(),
67077
67476
  overwriteWhenFull: boolean().nullable(),
67078
- preRecordSec: number().nullable(),
67079
- postRecordSec: number().nullable(),
67477
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67478
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
67080
67479
  /** Length of one recorded file, in minutes. */
67081
- segmentMinutes: number().nullable(),
67480
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
67082
67481
  /** The primary track's weekly windows, flattened. */
67083
67482
  windows: array(RecordWindowSchema),
67084
67483
  /**
@@ -67094,7 +67493,7 @@ var RecordingOnboardStatusSchema = object({
67094
67493
  * provider reporting a non-zero count also reports the schedule as not
67095
67494
  * writable.
67096
67495
  */
67097
- unreadableWindows: number(),
67496
+ unreadableWindows: number().meta(readingMeta("count")),
67098
67497
  /**
67099
67498
  * The camera is scheduled to record and has NO usable storage.
67100
67499
  *
@@ -68104,7 +68503,7 @@ var CamStreamDescriptorSchema = object({
68104
68503
  url: string().optional(),
68105
68504
  codec: string().optional(),
68106
68505
  resolution: CamStreamResolutionSchema.optional(),
68107
- fps: number().positive().optional(),
68506
+ fps: number().positive().optional().meta(readingMeta("rate")),
68108
68507
  label: string().optional(),
68109
68508
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
68110
68509
  deviceFeatures: array(string()).optional(),
@@ -68191,18 +68590,18 @@ var StreamProfileSchema = _enum([
68191
68590
  ]);
68192
68591
  /** The current encoding config of one stream profile. */
68193
68592
  var StreamProfileConfigSchema = object({
68194
- width: number(),
68195
- height: number(),
68593
+ width: number().meta(readingMeta("none")),
68594
+ height: number().meta(readingMeta("none")),
68196
68595
  codec: _enum(["h264", "h265"]),
68197
- framerate: number(),
68198
- bitrate: number(),
68596
+ framerate: number().meta(readingMeta("rate")),
68597
+ bitrate: number().meta(readingMeta("bitrate")),
68199
68598
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
68200
68599
  encoderProfile: _enum([
68201
68600
  "high",
68202
68601
  "main",
68203
68602
  "baseline"
68204
68603
  ]).optional(),
68205
- gop: number().optional(),
68604
+ gop: number().optional().meta(readingMeta("none")),
68206
68605
  /**
68207
68606
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
68208
68607
  *
@@ -68621,12 +69020,7 @@ var switchCapability = {
68621
69020
  label: "On",
68622
69021
  kind: "boolean"
68623
69022
  }
68624
- }] },
68625
- customize: {
68626
- version: 1,
68627
- advanced: "derive",
68628
- targets: []
68629
- }
69023
+ }] }
68630
69024
  };
68631
69025
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
68632
69026
  function hasControlCharacter(value) {
@@ -69209,35 +69603,21 @@ var tamperCapability = {
69209
69603
  * entries with `device_class: temperature` and any future native
69210
69604
  * thermometer.
69211
69605
  *
69212
- * Unit normalisation: providers convert to Celsius before storing.
69213
- * The slice value is always Celsius so cross-cap aggregators
69214
- * (climate-control's `currentTemp`, energy analytics) can compose
69215
- * without per-source unit fixups. The canonical display unit (`°C`) is
69216
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
69217
- * `sourceInfo`.
69606
+ * The number is Celsius (D944). Providers convert on ingest. A failed
69607
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
69218
69608
  *
69219
69609
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
69220
69610
  * frozen feed (provider hung) distinct from a "temperature hasn't
69221
69611
  * changed" steady state.
69222
69612
  */
69223
69613
  var TemperatureSensorStatusSchema = object({
69224
- /** Current temperature in Celsius. */
69225
- celsius: number().meta({
69226
- unit: "°C",
69614
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
69615
+ celsius: number().nullable().meta(readingMeta("temperature", {
69616
+ precision: 1,
69227
69617
  label: "Temperature"
69228
- }),
69618
+ })),
69229
69619
  /** Ms epoch when the slice was last updated (push or poll). */
69230
- lastFetchedAt: number(),
69231
- /** Live display unit from the upstream source (e.g. HA
69232
- * `attributes.unit_of_measurement`). The UI prefers this over the
69233
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
69234
- * Absent → the UI falls back to the role's canonical unit. */
69235
- unit: string().optional(),
69236
- /** Suggested decimal places for numeric display.
69237
- * Populated live from the upstream source when provided (e.g. HA
69238
- * `attributes.suggested_display_precision`). Falls back to
69239
- * auto-formatting when absent. */
69240
- precision: number().int().min(0).max(10).optional()
69620
+ lastFetchedAt: number()
69241
69621
  });
69242
69622
  var temperatureSensorCapability = {
69243
69623
  name: "temperature-sensor",
@@ -69268,12 +69648,7 @@ var temperatureSensorCapability = {
69268
69648
  } } },
69269
69649
  /** Clock fields: written, but excluded from the compare that decides
69270
69650
  * whether persisting is worth a SQLite commit. */
69271
- volatileStateFields: ["lastFetchedAt"],
69272
- customize: {
69273
- version: 1,
69274
- advanced: "derive",
69275
- targets: []
69276
- }
69651
+ volatileStateFields: ["lastFetchedAt"]
69277
69652
  };
69278
69653
  /**
69279
69654
  * toast — system-scoped singleton capability that streams toast
@@ -69556,16 +69931,16 @@ var VacuumControlStatusSchema = object({
69556
69931
  ]),
69557
69932
  /** 0..100 battery percentage. Null when the device has no battery
69558
69933
  * reading. */
69559
- batteryLevel: number().min(0).max(100).nullable(),
69934
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69560
69935
  /** Current fan-speed token (provider-verbatim). Null when unknown or
69561
69936
  * the vacuum has no speed control. */
69562
69937
  fanSpeed: string().nullable(),
69563
69938
  /** Speed tokens the hardware accepts — drives the UI selector. */
69564
69939
  availableFanSpeeds: array(string()),
69565
69940
  /** 0..100 cleaning-completion percentage of the current task, or null. */
69566
- progressPercent: number().min(0).max(100).nullable(),
69941
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69567
69942
  /** Current error code (0 / null = no error). */
69568
- errorCode: number().nullable(),
69943
+ errorCode: number().nullable().meta(readingMeta("none")),
69569
69944
  /** Human label for {@link errorCode}, or null when none / undecodable. */
69570
69945
  errorLabel: string().nullable(),
69571
69946
  /** Ms epoch when the slice was last updated. */
@@ -69643,7 +70018,7 @@ var ValveStatusSchema = object({
69643
70018
  ]),
69644
70019
  /** 0 = fully closed, 100 = fully open. Null when the device has no
69645
70020
  * intermediate position surface. */
69646
- position: number().min(0).max(100).nullable(),
70021
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69647
70022
  /** Ms epoch when the slice was last updated. */
69648
70023
  lastChangedAt: number()
69649
70024
  });
@@ -69751,9 +70126,9 @@ var vibrationCapability = {
69751
70126
  */
69752
70127
  var WaterHeaterStatusSchema = object({
69753
70128
  /** Current measured temperature. Null when not reported. */
69754
- currentTemp: number().nullable(),
70129
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69755
70130
  /** Target temperature setpoint. Null when no setpoint surface. */
69756
- targetTemp: number().nullable(),
70131
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69757
70132
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69758
70133
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69759
70134
  * device reports an unknown state. */
@@ -69764,9 +70139,9 @@ var WaterHeaterStatusSchema = object({
69764
70139
  * has no away surface. */
69765
70140
  away: boolean().nullable(),
69766
70141
  /** HA `min_temp` attribute. Null when not reported. */
69767
- minTemp: number().nullable(),
70142
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69768
70143
  /** HA `max_temp` attribute. Null when not reported. */
69769
- maxTemp: number().nullable(),
70144
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69770
70145
  /** Ms epoch when the slice was last updated. */
69771
70146
  lastChangedAt: number()
69772
70147
  });
@@ -69851,25 +70226,18 @@ var waterHeaterCapability = {
69851
70226
  * Forecast deferred — current conditions only for v1.
69852
70227
  */
69853
70228
  var WeatherStatusSchema = object({
69854
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69855
- * when no condition has been reported yet. */
70229
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69856
70230
  condition: string().nullable(),
69857
- /** Current temperature in the reported unit. Null when not provided. */
69858
- temperature: number().nullable(),
69859
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69860
- temperatureUnit: string().nullable(),
70231
+ /** Current temperature in Celsius. Null when not provided or refused. */
70232
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69861
70233
  /** Relative humidity (0..100). Null when not provided. */
69862
- humidity: number().min(0).max(100).nullable(),
69863
- /** Barometric pressure in the reported unit. Null when not provided. */
69864
- pressure: number().nullable(),
69865
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69866
- pressureUnit: string().nullable(),
69867
- /** Wind speed in the reported unit. Null when not provided. */
69868
- windSpeed: number().nullable(),
69869
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69870
- windSpeedUnit: string().nullable(),
70234
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
70235
+ /** Barometric pressure in hPa. Null when not provided or refused. */
70236
+ pressure: number().nullable().meta(readingMeta("pressure")),
70237
+ /** Wind speed in metres per second. Null when not provided or refused. */
70238
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69871
70239
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69872
- windBearing: number().nullable(),
70240
+ windBearing: number().nullable().meta(readingMeta("angle")),
69873
70241
  /** Ms epoch when the slice was last updated. */
69874
70242
  lastFetchedAt: number()
69875
70243
  });
@@ -69897,12 +70265,12 @@ var weatherCapability = {
69897
70265
  */
69898
70266
  durability: "session",
69899
70267
  /**
69900
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
70268
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69901
70269
  */
69902
70270
  history: { fields: {
69903
70271
  temperature: {
69904
70272
  kind: "gauge",
69905
- unitFrom: "temperatureUnit",
70273
+ unit: "°C",
69906
70274
  deadband: .2
69907
70275
  },
69908
70276
  humidity: {
@@ -69912,12 +70280,12 @@ var weatherCapability = {
69912
70280
  },
69913
70281
  pressure: {
69914
70282
  kind: "gauge",
69915
- unitFrom: "pressureUnit",
70283
+ unit: "hPa",
69916
70284
  deadband: .5
69917
70285
  },
69918
70286
  windSpeed: {
69919
70287
  kind: "gauge",
69920
- unitFrom: "windSpeedUnit",
70288
+ unit: "m/s",
69921
70289
  deadband: .5
69922
70290
  }
69923
70291
  } }
@@ -71296,6 +71664,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
71296
71664
  featureProbe: featureProbeCapability,
71297
71665
  flood: floodCapability,
71298
71666
  gas: gasCapability,
71667
+ groupCommands: groupCommandsCapability,
71668
+ groupStatus: groupStatusCapability,
71299
71669
  heatingValve: heatingValveCapability,
71300
71670
  humidifier: humidifierCapability,
71301
71671
  humiditySensor: humiditySensorCapability,
@@ -72672,6 +73042,13 @@ Object.freeze({
72672
73042
  access: "view",
72673
73043
  role: "admin"
72674
73044
  },
73045
+ "addons.getPackageDocs": {
73046
+ capName: "addons",
73047
+ capScope: "system",
73048
+ addonId: null,
73049
+ access: "view",
73050
+ role: "admin"
73051
+ },
72675
73052
  "addons.getVersions": {
72676
73053
  capName: "addons",
72677
73054
  capScope: "system",
@@ -75780,6 +76157,20 @@ Object.freeze({
75780
76157
  access: "view",
75781
76158
  role: "viewer"
75782
76159
  },
76160
+ "groupCommands.getStatus": {
76161
+ capName: "group-commands",
76162
+ capScope: "device",
76163
+ addonId: null,
76164
+ access: "view",
76165
+ role: "viewer"
76166
+ },
76167
+ "groupStatus.getStatus": {
76168
+ capName: "group-status",
76169
+ capScope: "device",
76170
+ addonId: null,
76171
+ access: "view",
76172
+ role: "viewer"
76173
+ },
75783
76174
  "heatingValve.getStatus": {
75784
76175
  capName: "heating-valve",
75785
76176
  capScope: "device",
@@ -85602,6 +85993,37 @@ var SENSOR_CAP_VOCABULARY = {
85602
85993
  icon: "flame"
85603
85994
  }]
85604
85995
  },
85996
+ "group-commands": {
85997
+ scope: "device",
85998
+ noun: t("Group command", "Comando di gruppo")
85999
+ },
86000
+ "group-status": {
86001
+ scope: "device",
86002
+ noun: t("Group", "Gruppo"),
86003
+ state: [
86004
+ {
86005
+ kind: "numeric",
86006
+ field: "included",
86007
+ quantity: t("the members reporting", "i membri che rispondono"),
86008
+ label: t("Reporting", "Rispondono"),
86009
+ icon: "users"
86010
+ },
86011
+ {
86012
+ kind: "numeric",
86013
+ field: "excluded",
86014
+ quantity: t("the members left out", "i membri esclusi"),
86015
+ label: t("Excluded", "Esclusi"),
86016
+ icon: "users"
86017
+ },
86018
+ {
86019
+ kind: "numeric",
86020
+ field: "total",
86021
+ quantity: t("the member count", "il numero di membri"),
86022
+ label: t("Members", "Membri"),
86023
+ icon: "sigma"
86024
+ }
86025
+ ]
86026
+ },
85605
86027
  "humidity-sensor": {
85606
86028
  scope: "device",
85607
86029
  noun: t("Humidity", "Umidità"),
@@ -85635,7 +86057,6 @@ var SENSOR_CAP_VOCABULARY = {
85635
86057
  field: "value",
85636
86058
  quantity: t("the reading", "il valore"),
85637
86059
  label: t("Reading", "Valore"),
85638
- unitField: "unit",
85639
86060
  icon: "gauge"
85640
86061
  }]
85641
86062
  },
@@ -85771,7 +86192,7 @@ var SENSOR_CAP_VOCABULARY = {
85771
86192
  field: "temperature",
85772
86193
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85773
86194
  label: t("Temperature", "Temperatura"),
85774
- unitField: "temperatureUnit",
86195
+ unit: "°C",
85775
86196
  icon: "thermometer"
85776
86197
  },
85777
86198
  {
@@ -85787,7 +86208,7 @@ var SENSOR_CAP_VOCABULARY = {
85787
86208
  field: "windSpeed",
85788
86209
  quantity: t("the wind speed", "la velocità del vento"),
85789
86210
  label: t("Wind", "Vento"),
85790
- unitField: "windSpeedUnit",
86211
+ unit: "m/s",
85791
86212
  icon: "wind"
85792
86213
  },
85793
86214
  {
@@ -85795,7 +86216,7 @@ var SENSOR_CAP_VOCABULARY = {
85795
86216
  field: "pressure",
85796
86217
  quantity: t("the air pressure", "la pressione atmosferica"),
85797
86218
  label: t("Pressure", "Pressione"),
85798
- unitField: "pressureUnit",
86219
+ unit: "hPa",
85799
86220
  icon: "gauge"
85800
86221
  }
85801
86222
  ]
@@ -87220,6 +87641,16 @@ Object.freeze({
87220
87641
  form: "single",
87221
87642
  optional: false
87222
87643
  }],
87644
+ "groupCommands.getStatus": [{
87645
+ name: "deviceId",
87646
+ form: "single",
87647
+ optional: false
87648
+ }],
87649
+ "groupStatus.getStatus": [{
87650
+ name: "deviceId",
87651
+ form: "single",
87652
+ optional: false
87653
+ }],
87223
87654
  "heatingValve.getStatus": [{
87224
87655
  name: "deviceId",
87225
87656
  form: "single",
@@ -89204,6 +89635,8 @@ new Map(Object.entries({
89204
89635
  "detection-blind": "device-events",
89205
89636
  "motion-noisy": "device-events",
89206
89637
  "motion-calm": "device-events",
89638
+ "package-llm-silent": "device-events",
89639
+ "package-llm-answered": "device-events",
89207
89640
  "recording-stalled": "device-events",
89208
89641
  "recording-healthy": "device-events",
89209
89642
  "recording-degraded": "device-events",
@@ -89287,6 +89720,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
89287
89720
  label: "Motion without a subject",
89288
89721
  kinds: ["motion-noisy", "motion-calm"]
89289
89722
  },
89723
+ {
89724
+ id: "package-llm",
89725
+ family: "device-events",
89726
+ label: "Package confirm",
89727
+ kinds: ["package-llm-silent", "package-llm-answered"]
89728
+ },
89290
89729
  {
89291
89730
  id: "recording",
89292
89731
  family: "device-events",
@@ -89468,6 +89907,8 @@ var DEVICE_EVENTS = [
89468
89907
  "detection-blind",
89469
89908
  "motion-noisy",
89470
89909
  "motion-calm",
89910
+ "package-llm-silent",
89911
+ "package-llm-answered",
89471
89912
  "recording-stalled",
89472
89913
  "recording-healthy",
89473
89914
  "recording-degraded",
@@ -89736,6 +90177,15 @@ new Map([
89736
90177
  snapshotLabel: "Cameras with constant motion now",
89737
90178
  snapshotSection: null
89738
90179
  },
90180
+ {
90181
+ id: "package-llm-confirm",
90182
+ family: "device-events",
90183
+ label: "Package confirm (not answering / answering again)",
90184
+ bad: ["package-llm-silent"],
90185
+ recovery: ["package-llm-answered"],
90186
+ snapshotLabel: "Cameras whose package confirm is not answering",
90187
+ snapshotSection: null
90188
+ },
89739
90189
  {
89740
90190
  id: "recording-health",
89741
90191
  family: "device-events",