@camstack/addon-provider-reolink 1.2.209 → 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. */
@@ -60089,6 +60463,7 @@ var doorbellCustomize = {
60089
60463
  applyAuthority: { kind: "composition-claim" },
60090
60464
  eventName: "onPressed",
60091
60465
  edges: ["rising"],
60466
+ defaultEdge: "rising",
60092
60467
  timestampPath: "lastPressedAt",
60093
60468
  counterPath: "pressCountSinceStart",
60094
60469
  highWater: "composer-advance",
@@ -60107,7 +60482,7 @@ var DoorbellStatusSchema = object({
60107
60482
  /** Ms epoch of the last press. null = never observed since this provider started. */
60108
60483
  lastPressedAt: number().nullable(),
60109
60484
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
60110
- pressCountSinceStart: number()
60485
+ pressCountSinceStart: number().meta(readingMeta("count"))
60111
60486
  });
60112
60487
  var DoorbellPressEventSchema = object({
60113
60488
  deviceId: number(),
@@ -60172,17 +60547,7 @@ onPressed: {
60172
60547
  history: "none",
60173
60548
  customize: doorbellCustomize
60174
60549
  };
60175
- /**
60176
- * Enum-state sensor — a string value picked from a finite option set.
60177
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
60178
- * states, weather conditions, contact-source channels, mode strings).
60179
- * The option set is stable across observations; it is persisted in the
60180
- * device config blob (via the control slice at adoption) and is NOT
60181
- * stored in `sourceInfo`.
60182
- */
60183
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
60184
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
60185
- var EnumSensorDateTimeFormatSchema = _enum([
60550
+ _enum([
60186
60551
  "date",
60187
60552
  "time",
60188
60553
  "datetime"
@@ -60190,12 +60555,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
60190
60555
  var EnumSensorStatusSchema = object({
60191
60556
  value: string(),
60192
60557
  /**
60193
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
60194
- * as a locale date/time/datetime; absent for plain enum sensors. HA
60195
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
60196
- * 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.
60197
60561
  */
60198
- format: EnumSensorDateTimeFormatSchema.optional(),
60562
+ timestamp: boolean().optional(),
60199
60563
  /** Ms epoch when the slice was last updated. */
60200
60564
  lastFetchedAt: number()
60201
60565
  });
@@ -60222,11 +60586,11 @@ var enumSensorCapability = {
60222
60586
  * History (D850): the value as an `enum` series (an appliance's program or
60223
60587
  * operation state, an HVAC action). The option set is per device, so the
60224
60588
  * value list is LEARNED as values are first seen (bounded). A sensor with a
60225
- * `format` carries a timestamp, not a state, and is not recorded.
60589
+ * `timestamp` carries a clock, not a state, and is not recorded.
60226
60590
  */
60227
60591
  history: { fields: { value: {
60228
60592
  kind: "enum",
60229
- skipWhenSet: "format"
60593
+ skipWhenSet: "timestamp"
60230
60594
  } } },
60231
60595
  /** Clock fields: written, but excluded from the compare that decides
60232
60596
  * whether persisting is worth a SQLite commit. */
@@ -60246,13 +60610,13 @@ var EventFireSchema = object({
60246
60610
  deviceId: number(),
60247
60611
  eventType: string(),
60248
60612
  data: record(string(), unknown()).nullable(),
60249
- timestamp: number(),
60250
- seq: number()
60613
+ timestamp: number().meta(readingMeta("none")),
60614
+ seq: number().meta(readingMeta("count"))
60251
60615
  });
60252
60616
  var EventEmitterStatusSchema = object({
60253
60617
  eventTypes: array(string()),
60254
60618
  lastEvent: EventFireSchema.nullable(),
60255
- eventCountSinceStart: number()
60619
+ eventCountSinceStart: number().meta(readingMeta("count"))
60256
60620
  });
60257
60621
  var eventEmitterCapability = {
60258
60622
  name: "event-emitter",
@@ -60665,10 +61029,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60665
61029
  var FanControlStatusSchema = object({
60666
61030
  /** Active speed as 0..100 inclusive. Null when the device has no
60667
61031
  * speed surface (single-speed fan). */
60668
- percentage: number().min(0).max(100).nullable(),
61032
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60669
61033
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60670
61034
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60671
- percentageStep: number().positive().optional(),
61035
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60672
61036
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60673
61037
  * — empty string when no preset is active or supported. */
60674
61038
  preset: string(),
@@ -60789,7 +61153,7 @@ var FeatureProbeStatusSchema = object({
60789
61153
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60790
61154
  model: string().nullable(),
60791
61155
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60792
- channelCount: number().nullable(),
61156
+ channelCount: number().nullable().meta(readingMeta("count")),
60793
61157
  /**
60794
61158
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60795
61159
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60912,9 +61276,91 @@ var gasCapability = {
60912
61276
  * whether persisting is worth a SQLite commit. */
60913
61277
  volatileStateFields: ["lastChangedAt"]
60914
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
+ };
60915
61361
  var HeatingValveStatusSchema = object({
60916
61362
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60917
- position: number().min(0).max(100).nullable(),
61363
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60918
61364
  /** Ms epoch when the slice was last updated (push or poll). */
60919
61365
  lastFetchedAt: number()
60920
61366
  });
@@ -60957,9 +61403,9 @@ var HumidifierStatusSchema = object({
60957
61403
  /** Whether the humidifier is currently on. */
60958
61404
  on: boolean(),
60959
61405
  /** Current measured relative humidity (0..100). Null when not reported. */
60960
- currentHumidity: number().min(0).max(100).nullable(),
61406
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60961
61407
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60962
- targetHumidity: number().min(0).max(100).nullable(),
61408
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60963
61409
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60964
61410
  * device has no mode surface. */
60965
61411
  mode: string().nullable(),
@@ -60969,9 +61415,9 @@ var HumidifierStatusSchema = object({
60969
61415
  * `idle` / `off`). Null when the device doesn't report it. */
60970
61416
  action: string().nullable(),
60971
61417
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60972
- minHumidity: number().nullable(),
61418
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60973
61419
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60974
- maxHumidity: number().nullable(),
61420
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60975
61421
  /** Ms epoch when the slice was last updated. */
60976
61422
  lastChangedAt: number()
60977
61423
  });
@@ -61041,22 +61487,13 @@ var humidifierCapability = {
61041
61487
  * `sourceInfo`.
61042
61488
  */
61043
61489
  var HumiditySensorStatusSchema = object({
61044
- /** Current relative humidity, 0..100. */
61045
- percent: number().min(0).max(100).meta({
61046
- 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,
61047
61493
  label: "Humidity"
61048
- }),
61494
+ })),
61049
61495
  /** Ms epoch when the slice was last updated. */
61050
- lastFetchedAt: number(),
61051
- /** Live display unit from the upstream source (e.g. HA
61052
- * `attributes.unit_of_measurement`). The UI prefers this over the
61053
- * role's canonical unit. Absent → fall back to the canonical unit. */
61054
- unit: string().optional(),
61055
- /** Suggested decimal places for numeric display.
61056
- * Populated live from the upstream source when provided (e.g. HA
61057
- * `attributes.suggested_display_precision`). Falls back to
61058
- * auto-formatting when absent. */
61059
- precision: number().int().min(0).max(10).optional()
61496
+ lastFetchedAt: number()
61060
61497
  });
61061
61498
  var humiditySensorCapability = {
61062
61499
  name: "humidity-sensor",
@@ -61087,12 +61524,7 @@ var humiditySensorCapability = {
61087
61524
  } } },
61088
61525
  /** Clock fields: written, but excluded from the compare that decides
61089
61526
  * whether persisting is worth a SQLite commit. */
61090
- volatileStateFields: ["lastFetchedAt"],
61091
- customize: {
61092
- version: 1,
61093
- advanced: "derive",
61094
- targets: []
61095
- }
61527
+ volatileStateFields: ["lastFetchedAt"]
61096
61528
  };
61097
61529
  /**
61098
61530
  * Image display cap. Models a single still image exposed by an integration —
@@ -61113,7 +61545,7 @@ var ImageStatusSchema = object({
61113
61545
  * entity exposes no `entity_picture` (yet). */
61114
61546
  url: string().nullable(),
61115
61547
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
61116
- lastUpdated: number().nullable()
61548
+ lastUpdated: number().nullable().meta(readingMeta("none"))
61117
61549
  });
61118
61550
  var imageCapability = {
61119
61551
  name: "image",
@@ -61201,19 +61633,19 @@ var NormalizedRangeSchema = object({
61201
61633
  */
61202
61634
  var ImageSettingsStatusSchema = object({
61203
61635
  /** Normalized 0–100. */
61204
- brightness: number().optional(),
61636
+ brightness: number().optional().meta(readingMeta("none")),
61205
61637
  /** Normalized 0–100. */
61206
- contrast: number().optional(),
61638
+ contrast: number().optional().meta(readingMeta("none")),
61207
61639
  /** Normalized 0–100. */
61208
- saturation: number().optional(),
61640
+ saturation: number().optional().meta(readingMeta("none")),
61209
61641
  /** Normalized 0–100. */
61210
- sharpness: number().optional(),
61642
+ sharpness: number().optional().meta(readingMeta("none")),
61211
61643
  mirror: boolean().optional(),
61212
61644
  flip: boolean().optional(),
61213
61645
  rotate: ImageRotateSchema.optional(),
61214
61646
  whiteBalance: WhiteBalanceModeSchema.optional(),
61215
61647
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
61216
- warmth: number().optional(),
61648
+ warmth: number().optional().meta(readingMeta("none")),
61217
61649
  exposureMode: ExposureModeSchema.optional(),
61218
61650
  backlightMode: BacklightModeSchema.optional(),
61219
61651
  lastFetchedAt: number()
@@ -61492,10 +61924,10 @@ var IntercomAbilitySchema = object({
61492
61924
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61493
61925
  codecs: array(string()),
61494
61926
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61495
- sampleRate: number(),
61927
+ sampleRate: number().meta(readingMeta("none")),
61496
61928
  duplex: _enum(["half", "full"]),
61497
61929
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61498
- maxBacklogMs: number()
61930
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61499
61931
  });
61500
61932
  var IntercomStatusSchema = object({
61501
61933
  /** True while a talk session is open. */
@@ -61722,13 +62154,13 @@ var LawnMowerControlStatusSchema = object({
61722
62154
  activity: LawnMowerActivitySchema,
61723
62155
  /** 0..100 battery percentage. Null when the device has no battery
61724
62156
  * reading. */
61725
- batteryLevel: number().min(0).max(100).nullable(),
62157
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61726
62158
  /** 0..100 mowing-completion percentage of the current task, or null when no
61727
62159
  * task is active / progress is unavailable. */
61728
- progressPercent: number().min(0).max(100).nullable(),
62160
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61729
62161
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61730
62162
  * or null when unknown. */
61731
- currentCode: number().nullable(),
62163
+ currentCode: number().nullable().meta(readingMeta("none")),
61732
62164
  /** Human label for {@link currentCode}, or null when undecodable. */
61733
62165
  currentCodeLabel: string().nullable(),
61734
62166
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62261,7 +62693,7 @@ var MediaPlayerStatusSchema = object({
62261
62693
  state: MediaPlayerStateSchema,
62262
62694
  /** Volume as 0..100 inclusive. Null when the device has no volume
62263
62695
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62264
- volumeLevel: number().min(0).max(100).nullable(),
62696
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62265
62697
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62266
62698
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62267
62699
  isMuted: boolean().nullable(),
@@ -62274,10 +62706,10 @@ var MediaPlayerStatusSchema = object({
62274
62706
  currentMedia: MediaInfoSchema.nullable(),
62275
62707
  /** Current playback position in ms. Null when not seekable or
62276
62708
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62277
- positionMs: number().int().nonnegative().nullable(),
62709
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62278
62710
  /** Total duration of the current media in ms. Null when unknown
62279
62711
  * (live stream) or nothing is playing. */
62280
- durationMs: number().int().nonnegative().nullable(),
62712
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62281
62713
  /** Shuffle toggle. Null when no shuffle surface.
62282
62714
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62283
62715
  shuffle: boolean().nullable(),
@@ -62586,7 +63018,7 @@ var motionCustomize = {
62586
63018
  eventName: "onMotionChanged"
62587
63019
  },
62588
63020
  deviceTypes: [DeviceType.Camera],
62589
- apply: "replace",
63021
+ apply: "add",
62590
63022
  applyAuthority: {
62591
63023
  kind: "camera-sensors",
62592
63024
  timeoutField: "motionCooldownMs"
@@ -62658,7 +63090,7 @@ var MotionStatusSchema = object({
62658
63090
  * it, every provider seeded it at boot, and every session left it
62659
63091
  * `detected: true` after its detach.
62660
63092
  */
62661
- autoClearAfterMs: number().nullable()
63093
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62662
63094
  });
62663
63095
  /**
62664
63096
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62695,12 +63127,18 @@ var motionCapability = {
62695
63127
  methods: {
62696
63128
  /**
62697
63129
  * Pull the current motion state synchronously. Convenience shortcut
62698
- * for consumers that don't need the full status object; equivalent
62699
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62700
- * `getStatus` once the auto-injected status surface lands in every
62701
- * 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.
62702
63132
  */
62703
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
+ } },
62704
63142
  events: {
62705
63143
  /**
62706
63144
  * Fires every time the runner transitions a camera between
@@ -62840,7 +63278,7 @@ var MotionZoneRegionSchema = object({
62840
63278
  * the grid region(s). */
62841
63279
  var MotionZoneStatusSchema = object({
62842
63280
  enabled: boolean(),
62843
- sensitivity: number(),
63281
+ sensitivity: number().meta(readingMeta("none")),
62844
63282
  /** Grid region(s). Today exactly one `grid` shape. */
62845
63283
  regions: array(MotionZoneRegionSchema),
62846
63284
  lastFetchedAt: number()
@@ -63210,7 +63648,7 @@ var NavigationStatusSchema = object({
63210
63648
  following: boolean(),
63211
63649
  flash: boolean(),
63212
63650
  lightMode: NavigationLightModeSchema,
63213
- lightLevel: number().min(40).max(100),
63651
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
63214
63652
  /** Ms epoch when the slice was last updated. */
63215
63653
  lastChangedAt: number()
63216
63654
  });
@@ -63344,13 +63782,13 @@ var NetworkLinkStatusSchema = object({
63344
63782
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63345
63783
  * SKIP a null rather than coerce it.
63346
63784
  */
63347
- signalPercent: number().min(0).max(100).nullable(),
63785
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63348
63786
  /** Raw received signal strength in dBm, when the firmware reports one. */
63349
- rssiDbm: number().optional(),
63787
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63350
63788
  /** Network name of a wireless link, when the firmware reports it. */
63351
63789
  ssid: string().optional(),
63352
63790
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63353
- lastUpdated: number()
63791
+ lastUpdated: number().meta(readingMeta("none"))
63354
63792
  });
63355
63793
  /** The slice a provider seeds before its first read: nothing is known yet. */
63356
63794
  var NETWORK_LINK_UNKNOWN = {
@@ -63755,7 +64193,7 @@ var NotifierStatusSchema = object({
63755
64193
  lastError: string().nullable(),
63756
64194
  /** Number of deliveries currently buffered server-side (rate-limited
63757
64195
  * notifiers may queue). 0 when the provider sends synchronously. */
63758
- queueDepth: number().int().nonnegative(),
64196
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63759
64197
  /** Per-feature capability matrix — authoritative source for the
63760
64198
  * compose-form field gating. */
63761
64199
  supports: NotifierSupportsSchema
@@ -63847,11 +64285,9 @@ onSent: { data: object({
63847
64285
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63848
64286
  * for: water flow, distance, weight, frequency, signal strength, …).
63849
64287
  *
63850
- * The `unit` and `precision` fields carry the live display metadata that
63851
- * the upstream source provides with each state push (HA
63852
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63853
- * The UI reads them directly from the slice — they are the single source
63854
- * 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).
63855
64291
  *
63856
64292
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63857
64293
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63860,15 +64296,8 @@ onSent: { data: object({
63860
64296
  * numeric values land here.
63861
64297
  */
63862
64298
  var NumericSensorStatusSchema = object({
63863
- value: number(),
63864
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63865
- * Populated live from the upstream source on each state push.
63866
- * Absent for unitless sensors. */
63867
- unit: string().optional(),
63868
- /** Suggested decimal places for numeric display.
63869
- * Populated live from the upstream source when provided.
63870
- * Falls back to auto-formatting when absent. */
63871
- 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")),
63872
64301
  /** Ms epoch when the slice was last updated. */
63873
64302
  lastFetchedAt: number()
63874
64303
  });
@@ -63892,11 +64321,11 @@ var numericSensorCapability = {
63892
64321
  */
63893
64322
  durability: "restored",
63894
64323
  /**
63895
- * 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.
63896
64326
  */
63897
64327
  history: { fields: { value: {
63898
64328
  kind: "gauge",
63899
- unitFrom: "unit",
63900
64329
  minIntervalMs: 3e4
63901
64330
  } } },
63902
64331
  /** Clock fields: written, but excluded from the compare that decides
@@ -64234,23 +64663,26 @@ var PetFeederStatusSchema = object({
64234
64663
  /** Food currently in the bowl (grams). Null when the device has not
64235
64664
  * reported a reading yet. On dual-hopper models this is the combined
64236
64665
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
64237
- foodLevel: number().nullable(),
64666
+ foodLevel: number().nullable().meta(readingMeta("none")),
64238
64667
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
64239
64668
  * single-hopper models. */
64240
- food1: number().nullable(),
64669
+ food1: number().nullable().meta(readingMeta("none")),
64241
64670
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
64242
64671
  * single-hopper models. */
64243
- food2: number().nullable(),
64672
+ food2: number().nullable().meta(readingMeta("none")),
64244
64673
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
64245
64674
  * (`device_class: problem`, on = low). True when the bowl is empty /
64246
64675
  * below the feeder's low threshold. */
64247
64676
  lowFood: boolean(),
64248
64677
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
64249
64678
  * device has no battery reading. */
64250
- batteryPower: number().min(0).max(100).nullable(),
64251
- /** Days of desiccant life remaining. Null when the model has no
64252
- * desiccant sensor. */
64253
- 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")),
64254
64686
  /** True while a feed is in progress. */
64255
64687
  feeding: boolean(),
64256
64688
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -64261,7 +64693,7 @@ var PetFeederStatusSchema = object({
64261
64693
  * with `errorCode` for consumers that want the raw integer. */
64262
64694
  error: string().nullable(),
64263
64695
  /** Raw device error code (0 / null = no error). */
64264
- errorCode: number().nullable(),
64696
+ errorCode: number().nullable().meta(readingMeta("none")),
64265
64697
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
64266
64698
  isDualHopper: boolean(),
64267
64699
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -64271,7 +64703,7 @@ var PetFeederStatusSchema = object({
64271
64703
  /** Play a chime when dispensing. */
64272
64704
  feedSound: boolean(),
64273
64705
  /** Speaker / prompt volume level (device-scaled integer). */
64274
- volume: number(),
64706
+ volume: number().meta(readingMeta("none")),
64275
64707
  /** Ms epoch when the slice was last refreshed from the cloud. */
64276
64708
  lastFetchedAt: number()
64277
64709
  });
@@ -64390,7 +64822,7 @@ var petFeederCapability = {
64390
64822
  },
64391
64823
  desiccantLeftDays: {
64392
64824
  kind: "gauge",
64393
- unit: "days"
64825
+ unit: "s"
64394
64826
  }
64395
64827
  } }
64396
64828
  };
@@ -64791,36 +65223,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64791
65223
  */
64792
65224
  var PowerMeterStatusSchema = object({
64793
65225
  /** Instantaneous power draw in watts. */
64794
- watts: number().optional().meta({
64795
- unit: "W",
64796
- label: "Power"
64797
- }),
65226
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64798
65227
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64799
- kwhTotal: number().optional().meta({
64800
- unit: "kWh",
64801
- label: "Energy"
64802
- }),
65228
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64803
65229
  /** Voltage in volts. */
64804
- volts: number().optional(),
65230
+ volts: number().optional().meta(readingMeta("voltage")),
64805
65231
  /** Current in amperes. */
64806
- amps: number().optional(),
64807
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64808
- va: number().optional(),
65232
+ amps: number().optional().meta(readingMeta("current")),
65233
+ /** Apparent power in volt-amperes. */
65234
+ va: number().optional().meta(readingMeta("apparent-power")),
64809
65235
  /** Power factor (0..1) if reported. */
64810
- powerFactor: number().min(0).max(1).optional(),
65236
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64811
65237
  /** Ms epoch when the slice was last updated. */
64812
- lastFetchedAt: number(),
64813
- /** Live display unit of the single metric this slice carries (e.g. HA
64814
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64815
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64816
- * unit per slice is unambiguous. The UI prefers this over the role's
64817
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64818
- unit: string().optional(),
64819
- /** Suggested decimal places for numeric display.
64820
- * Populated live from the upstream source when provided (e.g. HA
64821
- * `attributes.suggested_display_precision`). Falls back to
64822
- * auto-formatting when absent. */
64823
- precision: number().int().min(0).max(10).optional()
65238
+ lastFetchedAt: number()
64824
65239
  });
64825
65240
  var powerMeterCapability = {
64826
65241
  name: "power-meter",
@@ -64884,12 +65299,7 @@ var powerMeterCapability = {
64884
65299
  } },
64885
65300
  /** Clock fields: written, but excluded from the compare that decides
64886
65301
  * whether persisting is worth a SQLite commit. */
64887
- volatileStateFields: ["lastFetchedAt"],
64888
- customize: {
64889
- version: 1,
64890
- advanced: "derive",
64891
- targets: []
64892
- }
65302
+ volatileStateFields: ["lastFetchedAt"]
64893
65303
  };
64894
65304
  /**
64895
65305
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64909,11 +65319,11 @@ var powerMeterCapability = {
64909
65319
  */
64910
65320
  var GpsLocationSchema = object({
64911
65321
  /** Latitude in decimal degrees, -90..90. */
64912
- latitude: number().min(-90).max(90),
65322
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64913
65323
  /** Longitude in decimal degrees, -180..180. */
64914
- longitude: number().min(-180).max(180),
65324
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64915
65325
  /** Reported accuracy in meters (lower = better). */
64916
- accuracyMeters: number().nonnegative()
65326
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64917
65327
  });
64918
65328
  var PresenceStatusSchema = object({
64919
65329
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64925,7 +65335,7 @@ var PresenceStatusSchema = object({
64925
65335
  gps: GpsLocationSchema.nullable(),
64926
65336
  /** Optional battery level of the tracking device (0..100). Null
64927
65337
  * when not reported. */
64928
- batteryPercent: number().min(0).max(100).nullable(),
65338
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64929
65339
  /** Ms epoch when the slice was last updated. */
64930
65340
  lastChangedAt: number()
64931
65341
  });
@@ -64975,19 +65385,10 @@ var presenceCapability = {
64975
65385
  * `sourceInfo`.
64976
65386
  */
64977
65387
  var PressureSensorStatusSchema = object({
64978
- /** Current pressure in hPa. */
64979
- hpa: number(),
65388
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65389
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64980
65390
  /** Ms epoch when the slice was last updated. */
64981
- lastFetchedAt: number(),
64982
- /** Live display unit from the upstream source (e.g. HA
64983
- * `attributes.unit_of_measurement`). The UI prefers this over the
64984
- * role's canonical unit. Absent → fall back to the canonical unit. */
64985
- unit: string().optional(),
64986
- /** Suggested decimal places for numeric display.
64987
- * Populated live from the upstream source when provided (e.g. HA
64988
- * `attributes.suggested_display_precision`). Falls back to
64989
- * auto-formatting when absent. */
64990
- precision: number().int().min(0).max(10).optional()
65391
+ lastFetchedAt: number()
64991
65392
  });
64992
65393
  var pressureSensorCapability = {
64993
65394
  name: "pressure-sensor",
@@ -65513,8 +65914,8 @@ var ptzCapability = {
65513
65914
  };
65514
65915
  var PtzAutotrackSettingsSchema = object({
65515
65916
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65516
- stopDelaySeconds: number().int().min(0).max(300),
65517
- 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"))
65518
65919
  });
65519
65920
  /**
65520
65921
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65668,7 +66069,7 @@ var RunningStateStatusSchema = object({
65668
66069
  /** null = unknown — never coerced to false (D393). */
65669
66070
  running: boolean().nullable(),
65670
66071
  /** Epoch ms the current boolean began; null while `running` is null. */
65671
- since: number().nullable(),
66072
+ since: number().nullable().meta(readingMeta("none")),
65672
66073
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65673
66074
  phase: string().nullable(),
65674
66075
  /** null until a reading has been published (D393). */
@@ -65683,7 +66084,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65683
66084
  /** Generic Customize target for the running boolean (D936). */
65684
66085
  var runningCustomize = {
65685
66086
  version: 1,
65686
- advanced: "derive",
65687
66087
  targets: [{
65688
66088
  id: "running",
65689
66089
  cardinality: "scalar",
@@ -65703,7 +66103,7 @@ var runningCustomize = {
65703
66103
  DeviceType.Thermostat,
65704
66104
  DeviceType.PetFeeder
65705
66105
  ],
65706
- apply: "add-or-replace",
66106
+ apply: "add",
65707
66107
  path: "running",
65708
66108
  sources: [
65709
66109
  {
@@ -65719,6 +66119,7 @@ var runningCustomize = {
65719
66119
  requiredUnit: "W",
65720
66120
  transforms: [{
65721
66121
  kind: "threshold",
66122
+ comparisons: ["gte", "lte"],
65722
66123
  comparison: "gte",
65723
66124
  defaultThreshold: 5,
65724
66125
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -66995,9 +67396,9 @@ var RecordTriggerSchema = _enum([
66995
67396
  */
66996
67397
  var RecordWindowSchema = object({
66997
67398
  trigger: RecordTriggerSchema,
66998
- day: number().int().min(0).max(6),
66999
- startMinute: number().int().min(0).max(1439),
67000
- 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"))
67001
67402
  });
67002
67403
  /** Status of one physical volume, as the camera itself describes it. */
67003
67404
  var OnboardStorageVolumeSchema = object({
@@ -67073,10 +67474,10 @@ var RecordingOnboardStatusSchema = object({
67073
67474
  /** Master "record to the card at all" switch. */
67074
67475
  enabled: boolean().nullable(),
67075
67476
  overwriteWhenFull: boolean().nullable(),
67076
- preRecordSec: number().nullable(),
67077
- postRecordSec: number().nullable(),
67477
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67478
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
67078
67479
  /** Length of one recorded file, in minutes. */
67079
- segmentMinutes: number().nullable(),
67480
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
67080
67481
  /** The primary track's weekly windows, flattened. */
67081
67482
  windows: array(RecordWindowSchema),
67082
67483
  /**
@@ -67092,7 +67493,7 @@ var RecordingOnboardStatusSchema = object({
67092
67493
  * provider reporting a non-zero count also reports the schedule as not
67093
67494
  * writable.
67094
67495
  */
67095
- unreadableWindows: number(),
67496
+ unreadableWindows: number().meta(readingMeta("count")),
67096
67497
  /**
67097
67498
  * The camera is scheduled to record and has NO usable storage.
67098
67499
  *
@@ -68102,7 +68503,7 @@ var CamStreamDescriptorSchema = object({
68102
68503
  url: string().optional(),
68103
68504
  codec: string().optional(),
68104
68505
  resolution: CamStreamResolutionSchema.optional(),
68105
- fps: number().positive().optional(),
68506
+ fps: number().positive().optional().meta(readingMeta("rate")),
68106
68507
  label: string().optional(),
68107
68508
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
68108
68509
  deviceFeatures: array(string()).optional(),
@@ -68189,18 +68590,18 @@ var StreamProfileSchema = _enum([
68189
68590
  ]);
68190
68591
  /** The current encoding config of one stream profile. */
68191
68592
  var StreamProfileConfigSchema = object({
68192
- width: number(),
68193
- height: number(),
68593
+ width: number().meta(readingMeta("none")),
68594
+ height: number().meta(readingMeta("none")),
68194
68595
  codec: _enum(["h264", "h265"]),
68195
- framerate: number(),
68196
- bitrate: number(),
68596
+ framerate: number().meta(readingMeta("rate")),
68597
+ bitrate: number().meta(readingMeta("bitrate")),
68197
68598
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
68198
68599
  encoderProfile: _enum([
68199
68600
  "high",
68200
68601
  "main",
68201
68602
  "baseline"
68202
68603
  ]).optional(),
68203
- gop: number().optional(),
68604
+ gop: number().optional().meta(readingMeta("none")),
68204
68605
  /**
68205
68606
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
68206
68607
  *
@@ -68619,12 +69020,7 @@ var switchCapability = {
68619
69020
  label: "On",
68620
69021
  kind: "boolean"
68621
69022
  }
68622
- }] },
68623
- customize: {
68624
- version: 1,
68625
- advanced: "derive",
68626
- targets: []
68627
- }
69023
+ }] }
68628
69024
  };
68629
69025
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
68630
69026
  function hasControlCharacter(value) {
@@ -69207,35 +69603,21 @@ var tamperCapability = {
69207
69603
  * entries with `device_class: temperature` and any future native
69208
69604
  * thermometer.
69209
69605
  *
69210
- * Unit normalisation: providers convert to Celsius before storing.
69211
- * The slice value is always Celsius so cross-cap aggregators
69212
- * (climate-control's `currentTemp`, energy analytics) can compose
69213
- * without per-source unit fixups. The canonical display unit (`°C`) is
69214
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
69215
- * `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.
69216
69608
  *
69217
69609
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
69218
69610
  * frozen feed (provider hung) distinct from a "temperature hasn't
69219
69611
  * changed" steady state.
69220
69612
  */
69221
69613
  var TemperatureSensorStatusSchema = object({
69222
- /** Current temperature in Celsius. */
69223
- celsius: number().meta({
69224
- unit: "°C",
69614
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
69615
+ celsius: number().nullable().meta(readingMeta("temperature", {
69616
+ precision: 1,
69225
69617
  label: "Temperature"
69226
- }),
69618
+ })),
69227
69619
  /** Ms epoch when the slice was last updated (push or poll). */
69228
- lastFetchedAt: number(),
69229
- /** Live display unit from the upstream source (e.g. HA
69230
- * `attributes.unit_of_measurement`). The UI prefers this over the
69231
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
69232
- * Absent → the UI falls back to the role's canonical unit. */
69233
- unit: string().optional(),
69234
- /** Suggested decimal places for numeric display.
69235
- * Populated live from the upstream source when provided (e.g. HA
69236
- * `attributes.suggested_display_precision`). Falls back to
69237
- * auto-formatting when absent. */
69238
- precision: number().int().min(0).max(10).optional()
69620
+ lastFetchedAt: number()
69239
69621
  });
69240
69622
  var temperatureSensorCapability = {
69241
69623
  name: "temperature-sensor",
@@ -69266,12 +69648,7 @@ var temperatureSensorCapability = {
69266
69648
  } } },
69267
69649
  /** Clock fields: written, but excluded from the compare that decides
69268
69650
  * whether persisting is worth a SQLite commit. */
69269
- volatileStateFields: ["lastFetchedAt"],
69270
- customize: {
69271
- version: 1,
69272
- advanced: "derive",
69273
- targets: []
69274
- }
69651
+ volatileStateFields: ["lastFetchedAt"]
69275
69652
  };
69276
69653
  /**
69277
69654
  * toast — system-scoped singleton capability that streams toast
@@ -69554,16 +69931,16 @@ var VacuumControlStatusSchema = object({
69554
69931
  ]),
69555
69932
  /** 0..100 battery percentage. Null when the device has no battery
69556
69933
  * reading. */
69557
- batteryLevel: number().min(0).max(100).nullable(),
69934
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69558
69935
  /** Current fan-speed token (provider-verbatim). Null when unknown or
69559
69936
  * the vacuum has no speed control. */
69560
69937
  fanSpeed: string().nullable(),
69561
69938
  /** Speed tokens the hardware accepts — drives the UI selector. */
69562
69939
  availableFanSpeeds: array(string()),
69563
69940
  /** 0..100 cleaning-completion percentage of the current task, or null. */
69564
- progressPercent: number().min(0).max(100).nullable(),
69941
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69565
69942
  /** Current error code (0 / null = no error). */
69566
- errorCode: number().nullable(),
69943
+ errorCode: number().nullable().meta(readingMeta("none")),
69567
69944
  /** Human label for {@link errorCode}, or null when none / undecodable. */
69568
69945
  errorLabel: string().nullable(),
69569
69946
  /** Ms epoch when the slice was last updated. */
@@ -69641,7 +70018,7 @@ var ValveStatusSchema = object({
69641
70018
  ]),
69642
70019
  /** 0 = fully closed, 100 = fully open. Null when the device has no
69643
70020
  * intermediate position surface. */
69644
- position: number().min(0).max(100).nullable(),
70021
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69645
70022
  /** Ms epoch when the slice was last updated. */
69646
70023
  lastChangedAt: number()
69647
70024
  });
@@ -69749,9 +70126,9 @@ var vibrationCapability = {
69749
70126
  */
69750
70127
  var WaterHeaterStatusSchema = object({
69751
70128
  /** Current measured temperature. Null when not reported. */
69752
- currentTemp: number().nullable(),
70129
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69753
70130
  /** Target temperature setpoint. Null when no setpoint surface. */
69754
- targetTemp: number().nullable(),
70131
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69755
70132
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69756
70133
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69757
70134
  * device reports an unknown state. */
@@ -69762,9 +70139,9 @@ var WaterHeaterStatusSchema = object({
69762
70139
  * has no away surface. */
69763
70140
  away: boolean().nullable(),
69764
70141
  /** HA `min_temp` attribute. Null when not reported. */
69765
- minTemp: number().nullable(),
70142
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69766
70143
  /** HA `max_temp` attribute. Null when not reported. */
69767
- maxTemp: number().nullable(),
70144
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69768
70145
  /** Ms epoch when the slice was last updated. */
69769
70146
  lastChangedAt: number()
69770
70147
  });
@@ -69849,25 +70226,18 @@ var waterHeaterCapability = {
69849
70226
  * Forecast deferred — current conditions only for v1.
69850
70227
  */
69851
70228
  var WeatherStatusSchema = object({
69852
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69853
- * when no condition has been reported yet. */
70229
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69854
70230
  condition: string().nullable(),
69855
- /** Current temperature in the reported unit. Null when not provided. */
69856
- temperature: number().nullable(),
69857
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69858
- temperatureUnit: string().nullable(),
70231
+ /** Current temperature in Celsius. Null when not provided or refused. */
70232
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69859
70233
  /** Relative humidity (0..100). Null when not provided. */
69860
- humidity: number().min(0).max(100).nullable(),
69861
- /** Barometric pressure in the reported unit. Null when not provided. */
69862
- pressure: number().nullable(),
69863
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69864
- pressureUnit: string().nullable(),
69865
- /** Wind speed in the reported unit. Null when not provided. */
69866
- windSpeed: number().nullable(),
69867
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69868
- 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")),
69869
70239
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69870
- windBearing: number().nullable(),
70240
+ windBearing: number().nullable().meta(readingMeta("angle")),
69871
70241
  /** Ms epoch when the slice was last updated. */
69872
70242
  lastFetchedAt: number()
69873
70243
  });
@@ -69895,12 +70265,12 @@ var weatherCapability = {
69895
70265
  */
69896
70266
  durability: "session",
69897
70267
  /**
69898
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
70268
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69899
70269
  */
69900
70270
  history: { fields: {
69901
70271
  temperature: {
69902
70272
  kind: "gauge",
69903
- unitFrom: "temperatureUnit",
70273
+ unit: "°C",
69904
70274
  deadband: .2
69905
70275
  },
69906
70276
  humidity: {
@@ -69910,12 +70280,12 @@ var weatherCapability = {
69910
70280
  },
69911
70281
  pressure: {
69912
70282
  kind: "gauge",
69913
- unitFrom: "pressureUnit",
70283
+ unit: "hPa",
69914
70284
  deadband: .5
69915
70285
  },
69916
70286
  windSpeed: {
69917
70287
  kind: "gauge",
69918
- unitFrom: "windSpeedUnit",
70288
+ unit: "m/s",
69919
70289
  deadband: .5
69920
70290
  }
69921
70291
  } }
@@ -71294,6 +71664,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
71294
71664
  featureProbe: featureProbeCapability,
71295
71665
  flood: floodCapability,
71296
71666
  gas: gasCapability,
71667
+ groupCommands: groupCommandsCapability,
71668
+ groupStatus: groupStatusCapability,
71297
71669
  heatingValve: heatingValveCapability,
71298
71670
  humidifier: humidifierCapability,
71299
71671
  humiditySensor: humiditySensorCapability,
@@ -72670,6 +73042,13 @@ Object.freeze({
72670
73042
  access: "view",
72671
73043
  role: "admin"
72672
73044
  },
73045
+ "addons.getPackageDocs": {
73046
+ capName: "addons",
73047
+ capScope: "system",
73048
+ addonId: null,
73049
+ access: "view",
73050
+ role: "admin"
73051
+ },
72673
73052
  "addons.getVersions": {
72674
73053
  capName: "addons",
72675
73054
  capScope: "system",
@@ -75778,6 +76157,20 @@ Object.freeze({
75778
76157
  access: "view",
75779
76158
  role: "viewer"
75780
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
+ },
75781
76174
  "heatingValve.getStatus": {
75782
76175
  capName: "heating-valve",
75783
76176
  capScope: "device",
@@ -85600,6 +85993,37 @@ var SENSOR_CAP_VOCABULARY = {
85600
85993
  icon: "flame"
85601
85994
  }]
85602
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
+ },
85603
86027
  "humidity-sensor": {
85604
86028
  scope: "device",
85605
86029
  noun: t("Humidity", "Umidità"),
@@ -85633,7 +86057,6 @@ var SENSOR_CAP_VOCABULARY = {
85633
86057
  field: "value",
85634
86058
  quantity: t("the reading", "il valore"),
85635
86059
  label: t("Reading", "Valore"),
85636
- unitField: "unit",
85637
86060
  icon: "gauge"
85638
86061
  }]
85639
86062
  },
@@ -85769,7 +86192,7 @@ var SENSOR_CAP_VOCABULARY = {
85769
86192
  field: "temperature",
85770
86193
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85771
86194
  label: t("Temperature", "Temperatura"),
85772
- unitField: "temperatureUnit",
86195
+ unit: "°C",
85773
86196
  icon: "thermometer"
85774
86197
  },
85775
86198
  {
@@ -85785,7 +86208,7 @@ var SENSOR_CAP_VOCABULARY = {
85785
86208
  field: "windSpeed",
85786
86209
  quantity: t("the wind speed", "la velocità del vento"),
85787
86210
  label: t("Wind", "Vento"),
85788
- unitField: "windSpeedUnit",
86211
+ unit: "m/s",
85789
86212
  icon: "wind"
85790
86213
  },
85791
86214
  {
@@ -85793,7 +86216,7 @@ var SENSOR_CAP_VOCABULARY = {
85793
86216
  field: "pressure",
85794
86217
  quantity: t("the air pressure", "la pressione atmosferica"),
85795
86218
  label: t("Pressure", "Pressione"),
85796
- unitField: "pressureUnit",
86219
+ unit: "hPa",
85797
86220
  icon: "gauge"
85798
86221
  }
85799
86222
  ]
@@ -87218,6 +87641,16 @@ Object.freeze({
87218
87641
  form: "single",
87219
87642
  optional: false
87220
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
+ }],
87221
87654
  "heatingValve.getStatus": [{
87222
87655
  name: "deviceId",
87223
87656
  form: "single",
@@ -89082,6 +89515,11 @@ var PulseOverrideDraftSchema = object({
89082
89515
  targetId: string().min(1),
89083
89516
  enabled: boolean(),
89084
89517
  debounceMs: number().nonnegative(),
89518
+ edges: array(_enum([
89519
+ "rising",
89520
+ "button",
89521
+ "native-event"
89522
+ ])).min(1).optional(),
89085
89523
  sources: array(OverrideSourceSchema).min(1)
89086
89524
  });
89087
89525
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -89197,6 +89635,8 @@ new Map(Object.entries({
89197
89635
  "detection-blind": "device-events",
89198
89636
  "motion-noisy": "device-events",
89199
89637
  "motion-calm": "device-events",
89638
+ "package-llm-silent": "device-events",
89639
+ "package-llm-answered": "device-events",
89200
89640
  "recording-stalled": "device-events",
89201
89641
  "recording-healthy": "device-events",
89202
89642
  "recording-degraded": "device-events",
@@ -89280,6 +89720,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
89280
89720
  label: "Motion without a subject",
89281
89721
  kinds: ["motion-noisy", "motion-calm"]
89282
89722
  },
89723
+ {
89724
+ id: "package-llm",
89725
+ family: "device-events",
89726
+ label: "Package confirm",
89727
+ kinds: ["package-llm-silent", "package-llm-answered"]
89728
+ },
89283
89729
  {
89284
89730
  id: "recording",
89285
89731
  family: "device-events",
@@ -89461,6 +89907,8 @@ var DEVICE_EVENTS = [
89461
89907
  "detection-blind",
89462
89908
  "motion-noisy",
89463
89909
  "motion-calm",
89910
+ "package-llm-silent",
89911
+ "package-llm-answered",
89464
89912
  "recording-stalled",
89465
89913
  "recording-healthy",
89466
89914
  "recording-degraded",
@@ -89729,6 +90177,15 @@ new Map([
89729
90177
  snapshotLabel: "Cameras with constant motion now",
89730
90178
  snapshotSection: null
89731
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
+ },
89732
90189
  {
89733
90190
  id: "recording-health",
89734
90191
  family: "device-events",