@camstack/addon-import-alexa 0.2.147 → 0.2.148

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
@@ -38,7 +38,7 @@ let node_os = require("node:os");
38
38
  let node_fs = require("node:fs");
39
39
  let node_path = require("node:path");
40
40
  node_path = __toESM(node_path);
41
- //#region ../types/dist/event-category-DHXxB8t9.mjs
41
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
42
42
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
43
43
  EventCategory["SystemBoot"] = "system.boot";
44
44
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -364,6 +364,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
364
364
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
365
365
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
366
366
  /**
367
+ * A camera's package LLM confirm has failed closed three times in a row
368
+ * (D931, 2026-10-26). One edge per camera until
369
+ * {@link PipelinePackageLlmAnswered}.
370
+ */
371
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
372
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
373
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
374
+ /**
367
375
  * Per-camera pipeline config was mutated by the orchestrator
368
376
  * (a settings change via `setAgentInferenceDevices` /
369
377
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5606,7 +5614,7 @@ var ZodIssueCode = {
5606
5614
  var ZodFirstPartyTypeKind;
5607
5615
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5608
5616
  //#endregion
5609
- //#region ../types/dist/sleep-BErAxhul.mjs
5617
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5610
5618
  /**
5611
5619
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5612
5620
  * window to float samples (D455).
@@ -8348,6 +8356,99 @@ function defineEntryKind(name, spec) {
8348
8356
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8349
8357
  return defined;
8350
8358
  }
8359
+ var READING_QUANTITIES = [
8360
+ "temperature",
8361
+ "pressure",
8362
+ "speed",
8363
+ "precipitation",
8364
+ "precipitation-rate",
8365
+ "length",
8366
+ "area",
8367
+ "illuminance",
8368
+ "irradiance",
8369
+ "power",
8370
+ "energy",
8371
+ "voltage",
8372
+ "current",
8373
+ "apparent-power",
8374
+ "humidity",
8375
+ "percentage",
8376
+ "concentration-volume",
8377
+ "concentration-mass",
8378
+ "concentration-ppb",
8379
+ "volume",
8380
+ "duration",
8381
+ "duration-ms",
8382
+ "information",
8383
+ "sound-level",
8384
+ "signal-level",
8385
+ "colour-temperature",
8386
+ "angle",
8387
+ "bitrate",
8388
+ "rate",
8389
+ "mass",
8390
+ "index",
8391
+ "ratio",
8392
+ "count",
8393
+ "none",
8394
+ "device"
8395
+ ];
8396
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8397
+ new Set(READING_QUANTITIES);
8398
+ /**
8399
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8400
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8401
+ */
8402
+ var QUANTITY_BASE = {
8403
+ temperature: "°C",
8404
+ pressure: "hPa",
8405
+ speed: "m/s",
8406
+ precipitation: "mm",
8407
+ "precipitation-rate": "mm/h",
8408
+ length: "m",
8409
+ area: "m²",
8410
+ illuminance: "lx",
8411
+ irradiance: "W/m²",
8412
+ power: "W",
8413
+ energy: "kWh",
8414
+ voltage: "V",
8415
+ current: "A",
8416
+ "apparent-power": "VA",
8417
+ humidity: "%",
8418
+ percentage: "%",
8419
+ "concentration-volume": "ppm",
8420
+ "concentration-mass": "µg/m³",
8421
+ "concentration-ppb": "ppb",
8422
+ volume: "L",
8423
+ duration: "s",
8424
+ "duration-ms": "ms",
8425
+ information: "B",
8426
+ "sound-level": "dBFS",
8427
+ "signal-level": "dBm",
8428
+ "colour-temperature": "mired",
8429
+ angle: "°",
8430
+ bitrate: "kbps",
8431
+ rate: "fps",
8432
+ mass: "kg",
8433
+ index: "none",
8434
+ ratio: "none",
8435
+ count: "none",
8436
+ none: "none",
8437
+ device: "device"
8438
+ };
8439
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8440
+ function baseUnitOf(quantity) {
8441
+ const symbol = QUANTITY_BASE[quantity];
8442
+ if (symbol === "none" || symbol === "device") return void 0;
8443
+ return symbol;
8444
+ }
8445
+ function readingMeta(quantity, extras) {
8446
+ return {
8447
+ quantity,
8448
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8449
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8450
+ };
8451
+ }
8351
8452
  /** Shared Zod schemas used across streaming capabilities. */
8352
8453
  var CamProfileSchema = _enum([
8353
8454
  "high",
@@ -8375,8 +8476,8 @@ var CamStreamKindSchema = _enum([
8375
8476
  "derived"
8376
8477
  ]);
8377
8478
  var CamStreamResolutionSchema = object({
8378
- width: number().int().positive(),
8379
- height: number().int().positive()
8479
+ width: number().int().positive().meta(readingMeta("none")),
8480
+ height: number().int().positive().meta(readingMeta("none"))
8380
8481
  });
8381
8482
  var CameraStreamSchema = object({
8382
8483
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8894,6 +8995,8 @@ var CAP_NAMES = Object.freeze([
8894
8995
  "filesystem-browse",
8895
8996
  "flood",
8896
8997
  "gas",
8998
+ "group-commands",
8999
+ "group-status",
8897
9000
  "heating-valve",
8898
9001
  "humidifier",
8899
9002
  "humidity-sensor",
@@ -9023,14 +9126,6 @@ Object.freeze({
9023
9126
  {
9024
9127
  "path": "aqi",
9025
9128
  "valueType": "number"
9026
- },
9027
- {
9028
- "path": "unit",
9029
- "valueType": "string"
9030
- },
9031
- {
9032
- "path": "precision",
9033
- "valueType": "number"
9034
9129
  }
9035
9130
  ],
9036
9131
  "alarm-panel": [
@@ -9066,20 +9161,10 @@ Object.freeze({
9066
9161
  "valueType": "boolean"
9067
9162
  }
9068
9163
  ],
9069
- "ambient-light-sensor": [
9070
- {
9071
- "path": "lux",
9072
- "valueType": "number"
9073
- },
9074
- {
9075
- "path": "unit",
9076
- "valueType": "string"
9077
- },
9078
- {
9079
- "path": "precision",
9080
- "valueType": "number"
9081
- }
9082
- ],
9164
+ "ambient-light-sensor": [{
9165
+ "path": "lux",
9166
+ "valueType": "number"
9167
+ }],
9083
9168
  "audio-metrics": [
9084
9169
  {
9085
9170
  "path": "ts",
@@ -9159,6 +9244,10 @@ Object.freeze({
9159
9244
  "path": "lastUpdated",
9160
9245
  "valueType": "number"
9161
9246
  },
9247
+ {
9248
+ "path": "volts",
9249
+ "valueType": "number"
9250
+ },
9162
9251
  {
9163
9252
  "path": "lastContactAt",
9164
9253
  "valueType": "number"
@@ -9401,10 +9490,6 @@ Object.freeze({
9401
9490
  "datetime"
9402
9491
  ]
9403
9492
  },
9404
- {
9405
- "path": "unit",
9406
- "valueType": "string"
9407
- },
9408
9493
  {
9409
9494
  "path": "min",
9410
9495
  "valueType": "number"
@@ -9416,19 +9501,6 @@ Object.freeze({
9416
9501
  {
9417
9502
  "path": "step",
9418
9503
  "valueType": "number"
9419
- },
9420
- {
9421
- "path": "precision",
9422
- "valueType": "number"
9423
- },
9424
- {
9425
- "path": "format",
9426
- "valueType": "enum",
9427
- "enumValues": [
9428
- "date",
9429
- "time",
9430
- "datetime"
9431
- ]
9432
9504
  }
9433
9505
  ],
9434
9506
  "cover": [
@@ -9669,13 +9741,8 @@ Object.freeze({
9669
9741
  "path": "value",
9670
9742
  "valueType": "string"
9671
9743
  }, {
9672
- "path": "format",
9673
- "valueType": "enum",
9674
- "enumValues": [
9675
- "date",
9676
- "time",
9677
- "datetime"
9678
- ]
9744
+ "path": "timestamp",
9745
+ "valueType": "boolean"
9679
9746
  }],
9680
9747
  "event-emitter": [
9681
9748
  {
@@ -9748,6 +9815,20 @@ Object.freeze({
9748
9815
  "path": "detected",
9749
9816
  "valueType": "boolean"
9750
9817
  }],
9818
+ "group-status": [
9819
+ {
9820
+ "path": "included",
9821
+ "valueType": "number"
9822
+ },
9823
+ {
9824
+ "path": "excluded",
9825
+ "valueType": "number"
9826
+ },
9827
+ {
9828
+ "path": "total",
9829
+ "valueType": "number"
9830
+ }
9831
+ ],
9751
9832
  "heating-valve": [{
9752
9833
  "path": "position",
9753
9834
  "valueType": "number"
@@ -9782,20 +9863,10 @@ Object.freeze({
9782
9863
  "valueType": "number"
9783
9864
  }
9784
9865
  ],
9785
- "humidity-sensor": [
9786
- {
9787
- "path": "percent",
9788
- "valueType": "number"
9789
- },
9790
- {
9791
- "path": "unit",
9792
- "valueType": "string"
9793
- },
9794
- {
9795
- "path": "precision",
9796
- "valueType": "number"
9797
- }
9798
- ],
9866
+ "humidity-sensor": [{
9867
+ "path": "percent",
9868
+ "valueType": "number"
9869
+ }],
9799
9870
  "image": [{
9800
9871
  "path": "url",
9801
9872
  "valueType": "string"
@@ -10134,20 +10205,10 @@ Object.freeze({
10134
10205
  "valueType": "boolean"
10135
10206
  }
10136
10207
  ],
10137
- "numeric-sensor": [
10138
- {
10139
- "path": "value",
10140
- "valueType": "number"
10141
- },
10142
- {
10143
- "path": "unit",
10144
- "valueType": "string"
10145
- },
10146
- {
10147
- "path": "precision",
10148
- "valueType": "number"
10149
- }
10150
- ],
10208
+ "numeric-sensor": [{
10209
+ "path": "value",
10210
+ "valueType": "number"
10211
+ }],
10151
10212
  "pet-feeder": [
10152
10213
  {
10153
10214
  "path": "foodLevel",
@@ -10239,14 +10300,6 @@ Object.freeze({
10239
10300
  {
10240
10301
  "path": "powerFactor",
10241
10302
  "valueType": "number"
10242
- },
10243
- {
10244
- "path": "unit",
10245
- "valueType": "string"
10246
- },
10247
- {
10248
- "path": "precision",
10249
- "valueType": "number"
10250
10303
  }
10251
10304
  ],
10252
10305
  "presence": [
@@ -10275,20 +10328,10 @@ Object.freeze({
10275
10328
  "valueType": "number"
10276
10329
  }
10277
10330
  ],
10278
- "pressure-sensor": [
10279
- {
10280
- "path": "hpa",
10281
- "valueType": "number"
10282
- },
10283
- {
10284
- "path": "unit",
10285
- "valueType": "string"
10286
- },
10287
- {
10288
- "path": "precision",
10289
- "valueType": "number"
10290
- }
10291
- ],
10331
+ "pressure-sensor": [{
10332
+ "path": "hpa",
10333
+ "valueType": "number"
10334
+ }],
10292
10335
  "privacy-mask": [{
10293
10336
  "path": "enabled",
10294
10337
  "valueType": "boolean"
@@ -10599,20 +10642,10 @@ Object.freeze({
10599
10642
  "path": "tampered",
10600
10643
  "valueType": "boolean"
10601
10644
  }],
10602
- "temperature-sensor": [
10603
- {
10604
- "path": "celsius",
10605
- "valueType": "number"
10606
- },
10607
- {
10608
- "path": "unit",
10609
- "valueType": "string"
10610
- },
10611
- {
10612
- "path": "precision",
10613
- "valueType": "number"
10614
- }
10615
- ],
10645
+ "temperature-sensor": [{
10646
+ "path": "celsius",
10647
+ "valueType": "number"
10648
+ }],
10616
10649
  "update": [
10617
10650
  {
10618
10651
  "path": "currentVersion",
@@ -10723,10 +10756,6 @@ Object.freeze({
10723
10756
  "path": "temperature",
10724
10757
  "valueType": "number"
10725
10758
  },
10726
- {
10727
- "path": "temperatureUnit",
10728
- "valueType": "string"
10729
- },
10730
10759
  {
10731
10760
  "path": "humidity",
10732
10761
  "valueType": "number"
@@ -10735,18 +10764,10 @@ Object.freeze({
10735
10764
  "path": "pressure",
10736
10765
  "valueType": "number"
10737
10766
  },
10738
- {
10739
- "path": "pressureUnit",
10740
- "valueType": "string"
10741
- },
10742
10767
  {
10743
10768
  "path": "windSpeed",
10744
10769
  "valueType": "number"
10745
10770
  },
10746
- {
10747
- "path": "windSpeedUnit",
10748
- "valueType": "string"
10749
- },
10750
10771
  {
10751
10772
  "path": "windBearing",
10752
10773
  "valueType": "number"
@@ -11149,7 +11170,7 @@ function errMsg(err) {
11149
11170
  return String(err);
11150
11171
  }
11151
11172
  //#endregion
11152
- //#region ../types/dist/composition-kEl9mevu.mjs
11173
+ //#region ../types/dist/composition-OhpuespA.mjs
11153
11174
  /**
11154
11175
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11155
11176
  *
@@ -11904,15 +11925,74 @@ function validateExpressionSource(src) {
11904
11925
  }
11905
11926
  return null;
11906
11927
  }
11907
- /** A composed device's stableId is this prefix plus the block id. */
11908
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11909
- /** Longest snippet a `code` source may carry (slice 4). */
11910
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11928
+ /**
11929
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11930
+ * id. Split out so helper schemas can name it without a cycle through
11931
+ * `composition.ts` (D659).
11932
+ */
11911
11933
  var CompositionSourceRefSchema = object({
11912
11934
  /** The owning addon; `stableId` is unique only within it. */
11913
11935
  addonId: string().min(1),
11914
11936
  stableId: string().min(1)
11915
11937
  });
11938
+ var GROUP_TEMPLATE_IDS = [
11939
+ "temperature",
11940
+ "humidity",
11941
+ "pressure",
11942
+ "illuminance",
11943
+ "power",
11944
+ "energy",
11945
+ "battery",
11946
+ "numeric",
11947
+ "motion",
11948
+ "contact",
11949
+ "leak",
11950
+ "smoke",
11951
+ "occupancy",
11952
+ "binary",
11953
+ "switch",
11954
+ "light"
11955
+ ];
11956
+ var NUMERIC_AGGREGATES = [
11957
+ "mean",
11958
+ "median",
11959
+ "min",
11960
+ "max",
11961
+ "sum"
11962
+ ];
11963
+ var BINARY_AGGREGATES = ["any", "all"];
11964
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11965
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11966
+ var AGGREGATE_REDUCERS = [
11967
+ "mean",
11968
+ "median",
11969
+ "min",
11970
+ "max",
11971
+ "sum",
11972
+ "any",
11973
+ "all"
11974
+ ];
11975
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11976
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11977
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11978
+ /** One member. The field is stored so occupancy can name motion or binary. */
11979
+ var HelperMemberSchema = object({
11980
+ source: CompositionSourceRefSchema,
11981
+ cap: string().min(1),
11982
+ fieldPath: string().min(1)
11983
+ });
11984
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11985
+ kind: literal("group"),
11986
+ templateId: GroupTemplateIdSchema,
11987
+ aggregate: AggregateReducerSchema,
11988
+ members: array(HelperMemberSchema).min(1).max(64),
11989
+ /** Actuator groups only. Readings leave this unset. */
11990
+ active: ActiveCriterionSchema.optional()
11991
+ })]);
11992
+ /** A composed device's stableId is this prefix plus the block id. */
11993
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11994
+ /** Longest snippet a `code` source may carry (slice 4). */
11995
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11916
11996
  /** One field of one capability of one source device. */
11917
11997
  var CompositionFieldReadSchema = object({
11918
11998
  source: CompositionSourceRefSchema,
@@ -11956,7 +12036,33 @@ var CompositionCodeSourceSchema = object({
11956
12036
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11957
12037
  CompositionFromSourceSchema,
11958
12038
  CompositionExpressionSourceSchema,
11959
- CompositionCodeSourceSchema
12039
+ CompositionCodeSourceSchema,
12040
+ object({
12041
+ kind: literal("aggregate"),
12042
+ reducer: _enum([
12043
+ "mean",
12044
+ "median",
12045
+ "min",
12046
+ "max",
12047
+ "sum",
12048
+ "any",
12049
+ "all"
12050
+ ]),
12051
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12052
+ /** `value` is the reading; the others are the member counts. */
12053
+ publish: _enum([
12054
+ "value",
12055
+ "included",
12056
+ "excluded",
12057
+ "total"
12058
+ ]),
12059
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12060
+ empty: _enum([
12061
+ "null",
12062
+ "omit",
12063
+ "offline"
12064
+ ])
12065
+ })
11960
12066
  ]);
11961
12067
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11962
12068
  kind: literal("forward"),
@@ -12002,7 +12108,13 @@ var CompositionSchema = object({
12002
12108
  kind: literal("existing"),
12003
12109
  device: CompositionSourceRefSchema
12004
12110
  })]),
12005
- features: array(CompositionFeatureSchema).min(1).max(16)
12111
+ features: array(CompositionFeatureSchema).min(1).max(16),
12112
+ /**
12113
+ * Present when the block is a helper template. Absent on a free
12114
+ * composition (the room example, device 4603). The editor reads this,
12115
+ * never the compiled features.
12116
+ */
12117
+ helper: HelperSpecSchema.optional()
12006
12118
  }).superRefine((composition, ctx) => {
12007
12119
  const seen = /* @__PURE__ */ new Set();
12008
12120
  composition.features.forEach((feature, index) => {
@@ -18395,7 +18507,11 @@ var CompositionProblemSchema = object({
18395
18507
  "item-field-reserved",
18396
18508
  "target-missing",
18397
18509
  "target-unreadable",
18398
- "not-replaceable"
18510
+ "not-replaceable",
18511
+ "native-capability-replace",
18512
+ "mixed-quantity",
18513
+ "helper-mismatch",
18514
+ "member-constraint"
18399
18515
  ]),
18400
18516
  path: string(),
18401
18517
  message: string()
@@ -18416,7 +18532,8 @@ var CompositionUnavailableModeSchema = _enum([
18416
18532
  "null",
18417
18533
  "offline",
18418
18534
  "unknown",
18419
- "release"
18535
+ "release",
18536
+ "omit"
18420
18537
  ]);
18421
18538
  /**
18422
18539
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18453,9 +18570,11 @@ var CompositionFieldPlanSchema = object({
18453
18570
  reads: array(CompositionFieldReadSchema)
18454
18571
  });
18455
18572
  /**
18456
- * What a feature does to its target (D663): `new` builds a capability on a new
18457
- * device; on an existing device it `add`s a capability the device lacks, or
18458
- * `replace`s named fields of one it has natively.
18573
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18574
+ * new device; on an existing device it `add`s a capability the device lacks.
18575
+ * Replacing a capability the device already serves is refused
18576
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18577
+ * wire for a claim the hub already holds.
18459
18578
  */
18460
18579
  var CompositionFeatureModeSchema = _enum([
18461
18580
  "new",
@@ -18628,6 +18747,275 @@ Object.freeze({
18628
18747
  result: ["number", "null"]
18629
18748
  }
18630
18749
  });
18750
+ /**
18751
+ * One catalog row per base reading a group helper can build (D944 §3).
18752
+ * Data, not a form default written into the block.
18753
+ */
18754
+ var NOW = {
18755
+ fieldPath: "lastFetchedAt",
18756
+ expr: "now"
18757
+ };
18758
+ var CHANGED = {
18759
+ fieldPath: "lastChangedAt",
18760
+ expr: "now"
18761
+ };
18762
+ function numeric(input) {
18763
+ return {
18764
+ ...input,
18765
+ family: "numeric",
18766
+ deviceType: DeviceType.Sensor,
18767
+ memberDeviceType: DeviceType.Sensor,
18768
+ commandCaps: [],
18769
+ baseUnit: baseUnitOf(input.quantity),
18770
+ aggregates: NUMERIC_AGGREGATES,
18771
+ accept: [{
18772
+ cap: input.outputCap,
18773
+ fieldPath: input.outputField
18774
+ }]
18775
+ };
18776
+ }
18777
+ function binary(input) {
18778
+ return {
18779
+ ...input,
18780
+ family: "binary",
18781
+ deviceType: DeviceType.Sensor,
18782
+ memberDeviceType: DeviceType.Sensor,
18783
+ commandCaps: [],
18784
+ quantity: "none",
18785
+ baseUnit: void 0,
18786
+ aggregates: BINARY_AGGREGATES,
18787
+ empty: "offline"
18788
+ };
18789
+ }
18790
+ var GROUP_TEMPLATES = [
18791
+ numeric({
18792
+ id: "temperature",
18793
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18794
+ role: DeviceRole.TemperatureSensor,
18795
+ outputCap: "temperature-sensor",
18796
+ outputField: "celsius",
18797
+ quantity: "temperature",
18798
+ defaultAggregate: "mean",
18799
+ empty: "null",
18800
+ companions: [NOW]
18801
+ }),
18802
+ numeric({
18803
+ id: "humidity",
18804
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18805
+ role: DeviceRole.HumiditySensor,
18806
+ outputCap: "humidity-sensor",
18807
+ outputField: "percent",
18808
+ quantity: "humidity",
18809
+ defaultAggregate: "mean",
18810
+ empty: "null",
18811
+ companions: [NOW]
18812
+ }),
18813
+ numeric({
18814
+ id: "pressure",
18815
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18816
+ role: DeviceRole.PressureSensor,
18817
+ outputCap: "pressure-sensor",
18818
+ outputField: "hpa",
18819
+ quantity: "pressure",
18820
+ defaultAggregate: "mean",
18821
+ empty: "null",
18822
+ companions: [NOW]
18823
+ }),
18824
+ numeric({
18825
+ id: "illuminance",
18826
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18827
+ role: DeviceRole.AmbientLightSensor,
18828
+ outputCap: "ambient-light-sensor",
18829
+ outputField: "lux",
18830
+ quantity: "illuminance",
18831
+ defaultAggregate: "mean",
18832
+ empty: "null",
18833
+ companions: [NOW]
18834
+ }),
18835
+ numeric({
18836
+ id: "power",
18837
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18838
+ role: DeviceRole.PowerSensor,
18839
+ outputCap: "power-meter",
18840
+ outputField: "watts",
18841
+ quantity: "power",
18842
+ defaultAggregate: "sum",
18843
+ empty: "omit",
18844
+ companions: [NOW]
18845
+ }),
18846
+ numeric({
18847
+ id: "energy",
18848
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18849
+ role: DeviceRole.EnergySensor,
18850
+ outputCap: "power-meter",
18851
+ outputField: "kwhTotal",
18852
+ quantity: "energy",
18853
+ defaultAggregate: "sum",
18854
+ empty: "omit",
18855
+ companions: [NOW]
18856
+ }),
18857
+ numeric({
18858
+ id: "battery",
18859
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18860
+ role: DeviceRole.BatterySensor,
18861
+ outputCap: "battery",
18862
+ outputField: "percentage",
18863
+ quantity: "percentage",
18864
+ defaultAggregate: "mean",
18865
+ empty: "null",
18866
+ companions: [
18867
+ {
18868
+ fieldPath: "charging",
18869
+ expr: "\"unknown\""
18870
+ },
18871
+ {
18872
+ fieldPath: "sleeping",
18873
+ expr: "false"
18874
+ },
18875
+ {
18876
+ fieldPath: "lastUpdated",
18877
+ expr: "now"
18878
+ }
18879
+ ]
18880
+ }),
18881
+ numeric({
18882
+ id: "numeric",
18883
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18884
+ role: DeviceRole.NumericSensor,
18885
+ outputCap: "numeric-sensor",
18886
+ outputField: "value",
18887
+ quantity: "device",
18888
+ defaultAggregate: "mean",
18889
+ empty: "null",
18890
+ companions: [NOW]
18891
+ }),
18892
+ binary({
18893
+ id: "motion",
18894
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18895
+ role: DeviceRole.MotionSensor,
18896
+ outputCap: "motion",
18897
+ outputField: "detected",
18898
+ defaultAggregate: "any",
18899
+ accept: [{
18900
+ cap: "motion",
18901
+ fieldPath: "detected"
18902
+ }],
18903
+ companions: [{
18904
+ fieldPath: "lastDetectedAt",
18905
+ expr: "null"
18906
+ }, {
18907
+ fieldPath: "autoClearAfterMs",
18908
+ expr: "null"
18909
+ }]
18910
+ }),
18911
+ binary({
18912
+ id: "contact",
18913
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18914
+ role: DeviceRole.ContactSensor,
18915
+ outputCap: "contact",
18916
+ outputField: "entryOpen",
18917
+ defaultAggregate: "any",
18918
+ accept: [{
18919
+ cap: "contact",
18920
+ fieldPath: "entryOpen"
18921
+ }],
18922
+ companions: [CHANGED]
18923
+ }),
18924
+ binary({
18925
+ id: "leak",
18926
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18927
+ role: DeviceRole.LeakSensor,
18928
+ outputCap: "flood",
18929
+ outputField: "flooded",
18930
+ defaultAggregate: "any",
18931
+ accept: [{
18932
+ cap: "flood",
18933
+ fieldPath: "flooded"
18934
+ }],
18935
+ companions: [CHANGED]
18936
+ }),
18937
+ binary({
18938
+ id: "smoke",
18939
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18940
+ role: DeviceRole.SmokeSensor,
18941
+ outputCap: "smoke",
18942
+ outputField: "detected",
18943
+ defaultAggregate: "any",
18944
+ accept: [{
18945
+ cap: "smoke",
18946
+ fieldPath: "detected"
18947
+ }],
18948
+ companions: [CHANGED]
18949
+ }),
18950
+ binary({
18951
+ id: "occupancy",
18952
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18953
+ role: DeviceRole.BinarySensor,
18954
+ outputCap: "binary",
18955
+ outputField: "on",
18956
+ defaultAggregate: "any",
18957
+ accept: [{
18958
+ cap: "motion",
18959
+ fieldPath: "detected"
18960
+ }, {
18961
+ cap: "binary",
18962
+ fieldPath: "on"
18963
+ }],
18964
+ companions: [CHANGED]
18965
+ }),
18966
+ binary({
18967
+ id: "binary",
18968
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18969
+ role: DeviceRole.BinarySensor,
18970
+ outputCap: "binary",
18971
+ outputField: "on",
18972
+ defaultAggregate: "any",
18973
+ accept: [{
18974
+ cap: "binary",
18975
+ fieldPath: "on"
18976
+ }],
18977
+ companions: [CHANGED]
18978
+ }),
18979
+ actuator({
18980
+ id: "switch",
18981
+ 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.",
18982
+ deviceType: DeviceType.Switch,
18983
+ outputCap: "switch",
18984
+ commandCaps: ["switch"]
18985
+ }),
18986
+ actuator({
18987
+ id: "light",
18988
+ 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.",
18989
+ deviceType: DeviceType.Light,
18990
+ outputCap: "switch",
18991
+ commandCaps: [
18992
+ "switch",
18993
+ "brightness",
18994
+ "color"
18995
+ ]
18996
+ })
18997
+ ];
18998
+ function actuator(input) {
18999
+ return {
19000
+ ...input,
19001
+ family: "actuator",
19002
+ role: void 0,
19003
+ memberDeviceType: input.deviceType,
19004
+ outputField: "on",
19005
+ quantity: "none",
19006
+ baseUnit: void 0,
19007
+ aggregates: BINARY_AGGREGATES,
19008
+ defaultAggregate: "any",
19009
+ empty: "offline",
19010
+ accept: [{
19011
+ cap: "switch",
19012
+ fieldPath: "on"
19013
+ }],
19014
+ companions: [CHANGED]
19015
+ };
19016
+ }
19017
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19018
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18631
19019
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18632
19020
  /**
18633
19021
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20341,12 +20729,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20341
20729
  * providers detect renames by matching `uniqueId` and patch `id`
20342
20730
  * in-place so dispatch keeps working.
20343
20731
  *
20344
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20345
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20346
- * state push and are the single source of truth. The `device_class` tag is
20347
- * an HA-specific input that is consumed at the discovery boundary
20348
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20349
- * stored after that point.
20732
+ * **Display unit and precision are NOT stored here.** They are a read-time
20733
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20734
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20735
+ * neither is repeated on each sample. The `device_class` tag is an
20736
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20737
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20350
20738
  *
20351
20739
  * The `raw` escape hatch carries provider-specific attributes that don't
20352
20740
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20360,6 +20748,14 @@ var SourceInfoSchema = object({
20360
20748
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20361
20749
  * Used to detect rename when `id` changes. */
20362
20750
  uniqueId: string().optional(),
20751
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20752
+ quantity: ReadingQuantitySchema.optional(),
20753
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20754
+ dateTimeShape: _enum([
20755
+ "date",
20756
+ "time",
20757
+ "datetime"
20758
+ ]).optional(),
20363
20759
  /** Provider-specific extras that don't fit the structured slots.
20364
20760
  * Whitelist what goes here per provider — do NOT dump entire HA
20365
20761
  * attribute blobs (would explode the meta JSON column). */
@@ -41481,7 +41877,8 @@ method(object({
41481
41877
  "enum"
41482
41878
  ]),
41483
41879
  enumValues: array(string()).optional(),
41484
- item: boolean().optional()
41880
+ item: boolean().optional(),
41881
+ quantity: ReadingQuantitySchema.optional()
41485
41882
  })).readonly(),
41486
41883
  itemArray: object({
41487
41884
  path: string(),
@@ -45325,8 +45722,8 @@ var MaskPolygonShapeSchema = object({
45325
45722
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45326
45723
  var MaskGridShapeSchema = object({
45327
45724
  kind: literal("grid"),
45328
- gridWidth: number(),
45329
- gridHeight: number(),
45725
+ gridWidth: number().meta(readingMeta("none")),
45726
+ gridHeight: number().meta(readingMeta("none")),
45330
45727
  cells: array(boolean())
45331
45728
  });
45332
45729
  discriminatedUnion("kind", [
@@ -45438,6 +45835,8 @@ var NcSystemEventKindSchema = _enum([
45438
45835
  "detection-blind",
45439
45836
  "motion-noisy",
45440
45837
  "motion-calm",
45838
+ "package-llm-silent",
45839
+ "package-llm-answered",
45441
45840
  "recording-stalled",
45442
45841
  "recording-healthy",
45443
45842
  "recording-degraded",
@@ -56529,6 +56928,28 @@ var PackageVersionInfoSchema = object({
56529
56928
  deprecated: string().optional(),
56530
56929
  distTags: array(string()).readonly()
56531
56930
  });
56931
+ /**
56932
+ * One published document. `ok` is markdown from the registry. `missing` means
56933
+ * the registry answered and the file was not there. `error` is a registry or
56934
+ * tarball failure — never an empty string standing in for either.
56935
+ */
56936
+ var PackageDocBodySchema = discriminatedUnion("status", [
56937
+ object({
56938
+ status: literal("ok"),
56939
+ markdown: string()
56940
+ }),
56941
+ object({ status: literal("missing") }),
56942
+ object({
56943
+ status: literal("error"),
56944
+ message: string()
56945
+ })
56946
+ ]);
56947
+ var PackageDocsSchema = object({
56948
+ name: string(),
56949
+ version: string(),
56950
+ readme: PackageDocBodySchema,
56951
+ changelog: PackageDocBodySchema
56952
+ });
56532
56953
  var SearchResultSchema = object({
56533
56954
  name: string(),
56534
56955
  version: string(),
@@ -56747,7 +57168,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56747
57168
  }), object({ success: literal(true) }), {
56748
57169
  kind: "mutation",
56749
57170
  auth: "admin"
56750
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57171
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57172
+ name: string().min(1),
57173
+ version: string().min(1).optional()
57174
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56751
57175
  kind: "mutation",
56752
57176
  auth: "admin"
56753
57177
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56806,27 +57230,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56806
57230
  */
56807
57231
  var AirQualitySensorStatusSchema = object({
56808
57232
  /** Carbon dioxide concentration in ppm. */
56809
- co2Ppm: number().min(0).optional(),
57233
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56810
57234
  /** Total volatile organic compounds in ppb. */
56811
- vocPpb: number().min(0).optional(),
57235
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56812
57236
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56813
- pm25: number().min(0).optional(),
57237
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56814
57238
  /** Particulate matter ≤ 10 μm in µg/m³. */
56815
- pm10: number().min(0).optional(),
57239
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56816
57240
  /** Composite AQI value (typically 0..500). */
56817
- aqi: number().optional(),
57241
+ aqi: number().optional().meta(readingMeta("index")),
56818
57242
  /** Ms epoch when the slice was last updated. */
56819
- lastFetchedAt: number(),
56820
- /** Live display unit of the single metric this slice carries (e.g. HA
56821
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56822
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56823
- * per slice is unambiguous. */
56824
- unit: string().optional(),
56825
- /** Suggested decimal places for numeric display.
56826
- * Populated live from the upstream source when provided (e.g. HA
56827
- * `attributes.suggested_display_precision`). Falls back to
56828
- * auto-formatting when absent. */
56829
- precision: number().int().min(0).max(10).optional()
57243
+ lastFetchedAt: number()
56830
57244
  });
56831
57245
  var airQualitySensorCapability = {
56832
57246
  name: "air-quality-sensor",
@@ -56863,12 +57277,12 @@ var airQualitySensorCapability = {
56863
57277
  },
56864
57278
  pm25: {
56865
57279
  kind: "gauge",
56866
- unit: "ug/m3",
57280
+ unit: "µg/m³",
56867
57281
  deadband: 1
56868
57282
  },
56869
57283
  pm10: {
56870
57284
  kind: "gauge",
56871
- unit: "ug/m3",
57285
+ unit: "µg/m³",
56872
57286
  deadband: 1
56873
57287
  },
56874
57288
  aqi: {
@@ -56886,22 +57300,13 @@ var airQualitySensorCapability = {
56886
57300
  * entries with `device_class: illuminance`.
56887
57301
  */
56888
57302
  var AmbientLightSensorStatusSchema = object({
56889
- /** Current illuminance in lux (lx). */
56890
- lux: number().min(0).meta({
56891
- unit: "lx",
57303
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57304
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57305
+ precision: 0,
56892
57306
  label: "Illuminance"
56893
- }),
57307
+ })),
56894
57308
  /** Ms epoch when the slice was last updated. */
56895
- lastFetchedAt: number(),
56896
- /** Live display unit from the upstream source (e.g. HA
56897
- * `attributes.unit_of_measurement`). The UI prefers this over the
56898
- * role's canonical unit. Absent → fall back to the canonical unit. */
56899
- unit: string().optional(),
56900
- /** Suggested decimal places for numeric display.
56901
- * Populated live from the upstream source when provided (e.g. HA
56902
- * `attributes.suggested_display_precision`). Falls back to
56903
- * auto-formatting when absent. */
56904
- precision: number().int().min(0).max(10).optional()
57309
+ lastFetchedAt: number()
56905
57310
  });
56906
57311
  var ambientLightSensorCapability = {
56907
57312
  name: "ambient-light-sensor",
@@ -56933,12 +57338,7 @@ var ambientLightSensorCapability = {
56933
57338
  } } },
56934
57339
  /** Clock fields: written, but excluded from the compare that decides
56935
57340
  * whether persisting is worth a SQLite commit. */
56936
- volatileStateFields: ["lastFetchedAt"],
56937
- customize: {
56938
- version: 1,
56939
- advanced: "derive",
56940
- targets: []
56941
- }
57341
+ volatileStateFields: ["lastFetchedAt"]
56942
57342
  };
56943
57343
  /**
56944
57344
  * Per-class audio metrics aggregated over a sliding window.
@@ -57165,10 +57565,7 @@ var BatteryStatusSchema = object({
57165
57565
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57166
57566
  * declines to band a non-finite reading, which is the correct shape.
57167
57567
  */
57168
- percentage: number().min(0).max(100).nullable().meta({
57169
- unit: "%",
57170
- label: "Charge"
57171
- }),
57568
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57172
57569
  /**
57173
57570
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57174
57571
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57195,7 +57592,14 @@ var BatteryStatusSchema = object({
57195
57592
  */
57196
57593
  sleeping: boolean().meta({ label: "Sleeping" }),
57197
57594
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57198
- lastUpdated: number(),
57595
+ lastUpdated: number().meta(readingMeta("none")),
57596
+ /**
57597
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57598
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57599
+ * that only report charge. A second reading on this cap, not a second
57600
+ * `numeric-sensor` on the same device.
57601
+ */
57602
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57199
57603
  /**
57200
57604
  * Ms epoch of the last time the device PROVED it was reachable — a
57201
57605
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57247,7 +57651,6 @@ var BATTERY_DEVICE_TYPES = [
57247
57651
  /** Generic Customize target for the percentage reading (D936). */
57248
57652
  var batteryCustomize = {
57249
57653
  version: 1,
57250
- advanced: "derive",
57251
57654
  targets: [{
57252
57655
  id: "percentage",
57253
57656
  cardinality: "scalar",
@@ -57256,7 +57659,7 @@ var batteryCustomize = {
57256
57659
  stateField: "percentage"
57257
57660
  },
57258
57661
  deviceTypes: BATTERY_DEVICE_TYPES,
57259
- apply: "add-or-replace",
57662
+ apply: "add",
57260
57663
  path: "percentage",
57261
57664
  sources: [{
57262
57665
  kind: "field",
@@ -57377,10 +57780,16 @@ onStatusChanged: { data: object({
57377
57780
  /**
57378
57781
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57379
57782
  */
57380
- history: { fields: { percentage: {
57381
- kind: "gauge",
57382
- unit: "%"
57383
- } } },
57783
+ history: { fields: {
57784
+ percentage: {
57785
+ kind: "gauge",
57786
+ unit: "%"
57787
+ },
57788
+ volts: {
57789
+ kind: "gauge",
57790
+ unit: "V"
57791
+ }
57792
+ } },
57384
57793
  customize: batteryCustomize,
57385
57794
  /** Clock fields: written, but excluded from the compare that decides
57386
57795
  * whether persisting is worth a SQLite commit. */
@@ -57427,12 +57836,7 @@ var binaryCapability = {
57427
57836
  history: { fields: { on: { kind: "boolean" } } },
57428
57837
  /** Clock fields: written, but excluded from the compare that decides
57429
57838
  * whether persisting is worth a SQLite commit. */
57430
- volatileStateFields: ["lastChangedAt"],
57431
- customize: {
57432
- version: 1,
57433
- advanced: "derive",
57434
- targets: []
57435
- }
57839
+ volatileStateFields: ["lastChangedAt"]
57436
57840
  };
57437
57841
  /**
57438
57842
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57448,7 +57852,7 @@ var binaryCapability = {
57448
57852
  */
57449
57853
  var BrightnessStatusSchema = object({
57450
57854
  /** Current level as 0..100 inclusive. Firmware-reported. */
57451
- percentage: number().min(0).max(100),
57855
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57452
57856
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57453
57857
  lastChangedAt: number()
57454
57858
  });
@@ -57919,21 +58323,21 @@ var ClimateControlStatusSchema = object({
57919
58323
  /** Single setpoint in Celsius. Used by single-target modes
57920
58324
  * (heat / cool / dry). Null when the device is in a range mode
57921
58325
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57922
- target: number().nullable(),
58326
+ target: number().nullable().meta(readingMeta("temperature")),
57923
58327
  /** Upper bound of the dual setpoint in Celsius. Populated only
57924
58328
  * in `heat_cool` mode. */
57925
- targetHigh: number().nullable(),
58329
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57926
58330
  /** Lower bound of the dual setpoint in Celsius. Populated only
57927
58331
  * in `heat_cool` mode. */
57928
- targetLow: number().nullable(),
58332
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57929
58333
  /** Target relative humidity (0..100). Null when the device has no
57930
58334
  * humidity control. */
57931
- targetHumidity: number().min(0).max(100).nullable(),
58335
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57932
58336
  /** Current measured temperature in Celsius. Read-only — pushed by
57933
58337
  * the device's internal sensor. */
57934
- currentTemp: number().nullable(),
58338
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57935
58339
  /** Current measured relative humidity (0..100). Read-only. */
57936
- currentHumidity: number().min(0).max(100).nullable(),
58340
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57937
58341
  /** Vertical louver swing on/off. Null when the device has no vertical
57938
58342
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57939
58343
  swingVertical: boolean().nullable(),
@@ -57941,11 +58345,11 @@ var ClimateControlStatusSchema = object({
57941
58345
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57942
58346
  swingHorizontal: boolean().nullable(),
57943
58347
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57944
- minTemp: number().nullable().optional(),
58348
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57945
58349
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57946
- maxTemp: number().nullable().optional(),
58350
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57947
58351
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57948
- tempStep: number().positive().nullable().optional(),
58352
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57949
58353
  /** Ms epoch when the slice was last updated (push or command). */
57950
58354
  lastFetchedAt: number()
57951
58355
  });
@@ -58087,17 +58491,17 @@ var climateControlCapability = {
58087
58491
  * modes to expose in the picker without a slice fetch.
58088
58492
  */
58089
58493
  var RgbTripletSchema = object({
58090
- r: number().int().min(0).max(255),
58091
- g: number().int().min(0).max(255),
58092
- b: number().int().min(0).max(255)
58494
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58495
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58496
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58093
58497
  });
58094
58498
  var HsvTripletSchema = object({
58095
58499
  /** Hue in degrees, 0..360. */
58096
- h: number().min(0).max(360),
58500
+ h: number().min(0).max(360).meta(readingMeta("none")),
58097
58501
  /** Saturation as 0..100 inclusive. */
58098
- s: number().min(0).max(100),
58502
+ s: number().min(0).max(100).meta(readingMeta("none")),
58099
58503
  /** Value/brightness as 0..100 inclusive. */
58100
- v: number().min(0).max(100)
58504
+ v: number().min(0).max(100).meta(readingMeta("none"))
58101
58505
  });
58102
58506
  var ColorInputSchema = discriminatedUnion("mode", [
58103
58507
  object({
@@ -58126,7 +58530,7 @@ var ColorStatusSchema = object({
58126
58530
  /** Populated when `mode === 'hsv'`. */
58127
58531
  hsv: HsvTripletSchema.optional(),
58128
58532
  /** Populated when `mode === 'mired'`. */
58129
- mireds: number().int().optional(),
58533
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58130
58534
  /** Ms epoch of the last operator-driven change. */
58131
58535
  lastChangedAt: number()
58132
58536
  });
@@ -58321,18 +58725,12 @@ _enum([
58321
58725
  "empty",
58322
58726
  "full"
58323
58727
  ]);
58324
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58325
- * each. */
58326
- var ConsumableRemainingUnitSchema = _enum([
58327
- "days",
58328
- "hours",
58329
- "cycles"
58330
- ]);
58331
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58332
- * days). `value` null = unknown (D393). */
58728
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58729
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58730
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58333
58731
  var ConsumableRemainingSchema = object({
58334
- value: number().min(0).nullable(),
58335
- unit: ConsumableRemainingUnitSchema
58732
+ value: number().min(0).nullable().meta(readingMeta("device")),
58733
+ kind: ConsumableRemainingKindSchema
58336
58734
  });
58337
58735
  /** A single consumable item. Either a continuous `level` (remaining
58338
58736
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58345,7 +58743,7 @@ var ConsumableItemSchema = object({
58345
58743
  /** Display name. */
58346
58744
  label: string().min(1),
58347
58745
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58348
- level: number().min(0).max(100).nullable(),
58746
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58349
58747
  /** The provider's discrete verdict when known (binary mode). */
58350
58748
  status: ConsumableStatusSchema.nullable(),
58351
58749
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58427,8 +58825,8 @@ var consumablesCustomize = {
58427
58825
  transforms: [{ kind: "identity" }]
58428
58826
  }],
58429
58827
  literals: [{
58430
- path: "remaining.unit",
58431
- values: [...ConsumableRemainingUnitSchema.options]
58828
+ path: "remaining.kind",
58829
+ values: [...ConsumableRemainingKindSchema.options]
58432
58830
  }]
58433
58831
  },
58434
58832
  {
@@ -58557,7 +58955,6 @@ reset: method(object({
58557
58955
  },
58558
58956
  "remaining.value": {
58559
58957
  kind: "gauge",
58560
- unitFrom: "remaining.unit",
58561
58958
  deadband: 1,
58562
58959
  displayName: "Remaining"
58563
58960
  }
@@ -58627,10 +59024,9 @@ var contactCapability = {
58627
59024
  * (empty array for the other kinds). It lives in the slice because
58628
59025
  * HA `select.*` entities may update their options at runtime.
58629
59026
  *
58630
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58631
- * `kind === 'numeric'` and are absent (undefined) for all other
58632
- * kinds. They are populated live from HA attributes on each state
58633
- * push — consistent with `options` and the sensor-unit approach.
59027
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59028
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59029
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58634
59030
  */
58635
59031
  var ControlStatusSchema = object({
58636
59032
  kind: _enum([
@@ -58642,39 +59038,17 @@ var ControlStatusSchema = object({
58642
59038
  /** Current value. Numeric kind → number; select/text/datetime → string.
58643
59039
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58644
59040
  * or time-only — HA accepts all three forms). */
58645
- value: union([number(), string()]),
59041
+ value: union([number().meta(readingMeta("device")), string()]),
58646
59042
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58647
59043
  options: array(string()),
58648
59044
  /** Ms epoch when the slice was last updated. */
58649
59045
  lastChangedAt: number(),
58650
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58651
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58652
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58653
- unit: string().optional(),
58654
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58655
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58656
- min: number().optional(),
58657
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58658
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58659
- max: number().optional(),
58660
- /** Step increment. Populated live from HA `attributes.step`.
58661
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58662
- step: number().optional(),
58663
- /** Suggested decimal places for numeric display. Populated live from HA
58664
- * `attributes.suggested_display_precision`, or derived from `step` when the
58665
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58666
- * other kinds. Falls back to auto-formatting when absent. */
58667
- precision: number().int().min(0).max(10).optional(),
58668
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58669
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58670
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58671
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58672
- * `has_date`/`has_time` attributes on each push. */
58673
- format: _enum([
58674
- "date",
58675
- "time",
58676
- "datetime"
58677
- ]).optional()
59046
+ /** Minimum allowed value, in the device quantity's base unit. */
59047
+ min: number().optional().meta(readingMeta("device")),
59048
+ /** Maximum allowed value, in the device quantity's base unit. */
59049
+ max: number().optional().meta(readingMeta("device")),
59050
+ /** Step increment, in the device quantity's base unit. */
59051
+ step: number().optional().meta(readingMeta("device"))
58678
59052
  });
58679
59053
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58680
59054
  object({
@@ -58740,9 +59114,9 @@ var CoverStatusSchema = object({
58740
59114
  ]),
58741
59115
  /** 0 = fully closed, 100 = fully open. Null when the device has
58742
59116
  * no intermediate position surface. */
58743
- position: number().min(0).max(100).nullable(),
59117
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58744
59118
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58745
- tiltPosition: number().min(0).max(100).nullable(),
59119
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58746
59120
  /** Ms epoch when the slice was last updated. */
58747
59121
  lastChangedAt: number()
58748
59122
  });
@@ -58845,9 +59219,9 @@ var NormalizedRangeSchema$1 = object({
58845
59219
  var DayNightStatusSchema = object({
58846
59220
  mode: DayNightModeSchema,
58847
59221
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58848
- sensitivity: number().optional(),
59222
+ sensitivity: number().optional().meta(readingMeta("none")),
58849
59223
  /** Delay before the IR-cut filter flips, in seconds. */
58850
- switchDelaySec: number().optional(),
59224
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58851
59225
  lastFetchedAt: number()
58852
59226
  });
58853
59227
  /**
@@ -59065,11 +59439,11 @@ var DishwasherStatusSchema = object({
59065
59439
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59066
59440
  availablePrograms: array(string()),
59067
59441
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59068
- remainingSeconds: number().int().nonnegative().nullable(),
59442
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59069
59443
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59070
- delayedStartSeconds: number().int().nonnegative().nullable(),
59444
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59071
59445
  /** Cycle completion 0..100, or null when not applicable. */
59072
- progressPercent: number().min(0).max(100).nullable(),
59446
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59073
59447
  /** True when the door is reported open; null when the door state is not known. */
59074
59448
  doorOpen: boolean().nullable(),
59075
59449
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59093,7 +59467,7 @@ var DishwasherStatusSchema = object({
59093
59467
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59094
59468
  machineCareDue: boolean().nullable(),
59095
59469
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59096
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59470
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59097
59471
  /** Option states: null when the appliance does not expose the option or has not said. */
59098
59472
  halfLoad: boolean().nullable(),
59099
59473
  hygienePlus: boolean().nullable(),
@@ -59108,7 +59482,7 @@ var DishwasherStatusSchema = object({
59108
59482
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59109
59483
  activeEvents: array(string()),
59110
59484
  /** Numeric error code when the appliance reports one; null otherwise. */
59111
- errorCode: number().nullable(),
59485
+ errorCode: number().nullable().meta(readingMeta("none")),
59112
59486
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59113
59487
  errorLabel: string().nullable(),
59114
59488
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59339,7 +59713,7 @@ var DoorbellStatusSchema = object({
59339
59713
  /** Ms epoch of the last press. null = never observed since this provider started. */
59340
59714
  lastPressedAt: number().nullable(),
59341
59715
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59342
- pressCountSinceStart: number()
59716
+ pressCountSinceStart: number().meta(readingMeta("count"))
59343
59717
  });
59344
59718
  var DoorbellPressEventSchema = object({
59345
59719
  deviceId: number(),
@@ -59404,17 +59778,7 @@ onPressed: {
59404
59778
  history: "none",
59405
59779
  customize: doorbellCustomize
59406
59780
  };
59407
- /**
59408
- * Enum-state sensor — a string value picked from a finite option set.
59409
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59410
- * states, weather conditions, contact-source channels, mode strings).
59411
- * The option set is stable across observations; it is persisted in the
59412
- * device config blob (via the control slice at adoption) and is NOT
59413
- * stored in `sourceInfo`.
59414
- */
59415
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59416
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59417
- var EnumSensorDateTimeFormatSchema = _enum([
59781
+ _enum([
59418
59782
  "date",
59419
59783
  "time",
59420
59784
  "datetime"
@@ -59422,12 +59786,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59422
59786
  var EnumSensorStatusSchema = object({
59423
59787
  value: string(),
59424
59788
  /**
59425
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59426
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59427
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59428
- * a time-only sensor → `'time'`.
59789
+ * Set when `value` is a timestamp rather than an enum state, so history
59790
+ * does not learn ISO strings as states. The display shape (date / time /
59791
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59429
59792
  */
59430
- format: EnumSensorDateTimeFormatSchema.optional(),
59793
+ timestamp: boolean().optional(),
59431
59794
  /** Ms epoch when the slice was last updated. */
59432
59795
  lastFetchedAt: number()
59433
59796
  });
@@ -59454,11 +59817,11 @@ var enumSensorCapability = {
59454
59817
  * History (D850): the value as an `enum` series (an appliance's program or
59455
59818
  * operation state, an HVAC action). The option set is per device, so the
59456
59819
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59457
- * `format` carries a timestamp, not a state, and is not recorded.
59820
+ * `timestamp` carries a clock, not a state, and is not recorded.
59458
59821
  */
59459
59822
  history: { fields: { value: {
59460
59823
  kind: "enum",
59461
- skipWhenSet: "format"
59824
+ skipWhenSet: "timestamp"
59462
59825
  } } },
59463
59826
  /** Clock fields: written, but excluded from the compare that decides
59464
59827
  * whether persisting is worth a SQLite commit. */
@@ -59478,13 +59841,13 @@ var EventFireSchema = object({
59478
59841
  deviceId: number(),
59479
59842
  eventType: string(),
59480
59843
  data: record(string(), unknown()).nullable(),
59481
- timestamp: number(),
59482
- seq: number()
59844
+ timestamp: number().meta(readingMeta("none")),
59845
+ seq: number().meta(readingMeta("count"))
59483
59846
  });
59484
59847
  var EventEmitterStatusSchema = object({
59485
59848
  eventTypes: array(string()),
59486
59849
  lastEvent: EventFireSchema.nullable(),
59487
- eventCountSinceStart: number()
59850
+ eventCountSinceStart: number().meta(readingMeta("count"))
59488
59851
  });
59489
59852
  var eventEmitterCapability = {
59490
59853
  name: "event-emitter",
@@ -59897,10 +60260,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59897
60260
  var FanControlStatusSchema = object({
59898
60261
  /** Active speed as 0..100 inclusive. Null when the device has no
59899
60262
  * speed surface (single-speed fan). */
59900
- percentage: number().min(0).max(100).nullable(),
60263
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59901
60264
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59902
60265
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59903
- percentageStep: number().positive().optional(),
60266
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59904
60267
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59905
60268
  * — empty string when no preset is active or supported. */
59906
60269
  preset: string(),
@@ -60021,7 +60384,7 @@ var FeatureProbeStatusSchema = object({
60021
60384
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60022
60385
  model: string().nullable(),
60023
60386
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60024
- channelCount: number().nullable(),
60387
+ channelCount: number().nullable().meta(readingMeta("count")),
60025
60388
  /**
60026
60389
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60027
60390
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60144,9 +60507,91 @@ var gasCapability = {
60144
60507
  * whether persisting is worth a SQLite commit. */
60145
60508
  volatileStateFields: ["lastChangedAt"]
60146
60509
  };
60510
+ var GroupCommandsSchema = object({
60511
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60512
+ results: array(object({
60513
+ deviceId: number().int().nonnegative(),
60514
+ cap: string().min(1),
60515
+ method: string().min(1),
60516
+ ok: boolean(),
60517
+ error: string().optional(),
60518
+ /** Ms epoch of the attempt. */
60519
+ at: number()
60520
+ })) });
60521
+ var groupCommandsCapability = {
60522
+ name: "group-commands",
60523
+ scope: "device",
60524
+ deviceNative: true,
60525
+ mode: "singleton",
60526
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60527
+ methods: {},
60528
+ status: {
60529
+ schema: GroupCommandsSchema,
60530
+ kind: "push"
60531
+ },
60532
+ runtimeState: GroupCommandsSchema,
60533
+ /**
60534
+ * Runtime-state durability: **session** — the last attempt. A restored
60535
+ * failure would outlive the command that caused it.
60536
+ *
60537
+ * See `RuntimeStateDurability`. Enforced by
60538
+ * `scripts/check-runtime-state-durability.ts`.
60539
+ */
60540
+ durability: "session",
60541
+ history: "none"
60542
+ };
60543
+ /**
60544
+ * How many members of a helper group answered (D944). Counts are integers,
60545
+ * not measurements: zero included is a real count, never a stand-in reading.
60546
+ * The reading itself stays on the output capability. History is none — the
60547
+ * series that later statistics read is the output cap's own history block.
60548
+ */
60549
+ var GroupStatusSchema = object({
60550
+ /** Members whose current value entered the aggregate. */
60551
+ included: number().int().nonnegative().meta(readingMeta("count", {
60552
+ precision: 0,
60553
+ label: "Reporting"
60554
+ })),
60555
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60556
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60557
+ precision: 0,
60558
+ label: "Excluded"
60559
+ })),
60560
+ /** `included` + `excluded`. */
60561
+ total: number().int().nonnegative().meta(readingMeta("count", {
60562
+ precision: 0,
60563
+ label: "Members"
60564
+ }))
60565
+ });
60566
+ var groupStatusCapability = {
60567
+ name: "group-status",
60568
+ scope: "device",
60569
+ deviceNative: true,
60570
+ mode: "singleton",
60571
+ deviceTypes: [
60572
+ DeviceType.Sensor,
60573
+ DeviceType.Switch,
60574
+ DeviceType.Light
60575
+ ],
60576
+ methods: {},
60577
+ status: {
60578
+ schema: GroupStatusSchema,
60579
+ kind: "push"
60580
+ },
60581
+ runtimeState: GroupStatusSchema,
60582
+ /**
60583
+ * Runtime-state durability: **session** — recomputed from members on the
60584
+ * next source event; a restored count would outlive the readings it named.
60585
+ *
60586
+ * See `RuntimeStateDurability`. Enforced by
60587
+ * `scripts/check-runtime-state-durability.ts`.
60588
+ */
60589
+ durability: "session",
60590
+ history: "none"
60591
+ };
60147
60592
  var HeatingValveStatusSchema = object({
60148
60593
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60149
- position: number().min(0).max(100).nullable(),
60594
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60150
60595
  /** Ms epoch when the slice was last updated (push or poll). */
60151
60596
  lastFetchedAt: number()
60152
60597
  });
@@ -60189,9 +60634,9 @@ var HumidifierStatusSchema = object({
60189
60634
  /** Whether the humidifier is currently on. */
60190
60635
  on: boolean(),
60191
60636
  /** Current measured relative humidity (0..100). Null when not reported. */
60192
- currentHumidity: number().min(0).max(100).nullable(),
60637
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60193
60638
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60194
- targetHumidity: number().min(0).max(100).nullable(),
60639
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60195
60640
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60196
60641
  * device has no mode surface. */
60197
60642
  mode: string().nullable(),
@@ -60201,9 +60646,9 @@ var HumidifierStatusSchema = object({
60201
60646
  * `idle` / `off`). Null when the device doesn't report it. */
60202
60647
  action: string().nullable(),
60203
60648
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60204
- minHumidity: number().nullable(),
60649
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60205
60650
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60206
- maxHumidity: number().nullable(),
60651
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60207
60652
  /** Ms epoch when the slice was last updated. */
60208
60653
  lastChangedAt: number()
60209
60654
  });
@@ -60273,22 +60718,13 @@ var humidifierCapability = {
60273
60718
  * `sourceInfo`.
60274
60719
  */
60275
60720
  var HumiditySensorStatusSchema = object({
60276
- /** Current relative humidity, 0..100. */
60277
- percent: number().min(0).max(100).meta({
60278
- unit: "%",
60721
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60722
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60723
+ precision: 0,
60279
60724
  label: "Humidity"
60280
- }),
60725
+ })),
60281
60726
  /** Ms epoch when the slice was last updated. */
60282
- lastFetchedAt: number(),
60283
- /** Live display unit from the upstream source (e.g. HA
60284
- * `attributes.unit_of_measurement`). The UI prefers this over the
60285
- * role's canonical unit. Absent → fall back to the canonical unit. */
60286
- unit: string().optional(),
60287
- /** Suggested decimal places for numeric display.
60288
- * Populated live from the upstream source when provided (e.g. HA
60289
- * `attributes.suggested_display_precision`). Falls back to
60290
- * auto-formatting when absent. */
60291
- precision: number().int().min(0).max(10).optional()
60727
+ lastFetchedAt: number()
60292
60728
  });
60293
60729
  var humiditySensorCapability = {
60294
60730
  name: "humidity-sensor",
@@ -60319,12 +60755,7 @@ var humiditySensorCapability = {
60319
60755
  } } },
60320
60756
  /** Clock fields: written, but excluded from the compare that decides
60321
60757
  * whether persisting is worth a SQLite commit. */
60322
- volatileStateFields: ["lastFetchedAt"],
60323
- customize: {
60324
- version: 1,
60325
- advanced: "derive",
60326
- targets: []
60327
- }
60758
+ volatileStateFields: ["lastFetchedAt"]
60328
60759
  };
60329
60760
  /**
60330
60761
  * Image display cap. Models a single still image exposed by an integration —
@@ -60345,7 +60776,7 @@ var ImageStatusSchema = object({
60345
60776
  * entity exposes no `entity_picture` (yet). */
60346
60777
  url: string().nullable(),
60347
60778
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60348
- lastUpdated: number().nullable()
60779
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60349
60780
  });
60350
60781
  var imageCapability = {
60351
60782
  name: "image",
@@ -60433,19 +60864,19 @@ var NormalizedRangeSchema = object({
60433
60864
  */
60434
60865
  var ImageSettingsStatusSchema = object({
60435
60866
  /** Normalized 0–100. */
60436
- brightness: number().optional(),
60867
+ brightness: number().optional().meta(readingMeta("none")),
60437
60868
  /** Normalized 0–100. */
60438
- contrast: number().optional(),
60869
+ contrast: number().optional().meta(readingMeta("none")),
60439
60870
  /** Normalized 0–100. */
60440
- saturation: number().optional(),
60871
+ saturation: number().optional().meta(readingMeta("none")),
60441
60872
  /** Normalized 0–100. */
60442
- sharpness: number().optional(),
60873
+ sharpness: number().optional().meta(readingMeta("none")),
60443
60874
  mirror: boolean().optional(),
60444
60875
  flip: boolean().optional(),
60445
60876
  rotate: ImageRotateSchema.optional(),
60446
60877
  whiteBalance: WhiteBalanceModeSchema.optional(),
60447
60878
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60448
- warmth: number().optional(),
60879
+ warmth: number().optional().meta(readingMeta("none")),
60449
60880
  exposureMode: ExposureModeSchema.optional(),
60450
60881
  backlightMode: BacklightModeSchema.optional(),
60451
60882
  lastFetchedAt: number()
@@ -60724,10 +61155,10 @@ var IntercomAbilitySchema = object({
60724
61155
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60725
61156
  codecs: array(string()),
60726
61157
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60727
- sampleRate: number(),
61158
+ sampleRate: number().meta(readingMeta("none")),
60728
61159
  duplex: _enum(["half", "full"]),
60729
61160
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60730
- maxBacklogMs: number()
61161
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60731
61162
  });
60732
61163
  var IntercomStatusSchema = object({
60733
61164
  /** True while a talk session is open. */
@@ -60954,13 +61385,13 @@ var LawnMowerControlStatusSchema = object({
60954
61385
  activity: LawnMowerActivitySchema,
60955
61386
  /** 0..100 battery percentage. Null when the device has no battery
60956
61387
  * reading. */
60957
- batteryLevel: number().min(0).max(100).nullable(),
61388
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60958
61389
  /** 0..100 mowing-completion percentage of the current task, or null when no
60959
61390
  * task is active / progress is unavailable. */
60960
- progressPercent: number().min(0).max(100).nullable(),
61391
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60961
61392
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60962
61393
  * or null when unknown. */
60963
- currentCode: number().nullable(),
61394
+ currentCode: number().nullable().meta(readingMeta("none")),
60964
61395
  /** Human label for {@link currentCode}, or null when undecodable. */
60965
61396
  currentCodeLabel: string().nullable(),
60966
61397
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61493,7 +61924,7 @@ var MediaPlayerStatusSchema = object({
61493
61924
  state: MediaPlayerStateSchema,
61494
61925
  /** Volume as 0..100 inclusive. Null when the device has no volume
61495
61926
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61496
- volumeLevel: number().min(0).max(100).nullable(),
61927
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61497
61928
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61498
61929
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61499
61930
  isMuted: boolean().nullable(),
@@ -61506,10 +61937,10 @@ var MediaPlayerStatusSchema = object({
61506
61937
  currentMedia: MediaInfoSchema.nullable(),
61507
61938
  /** Current playback position in ms. Null when not seekable or
61508
61939
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61509
- positionMs: number().int().nonnegative().nullable(),
61940
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61510
61941
  /** Total duration of the current media in ms. Null when unknown
61511
61942
  * (live stream) or nothing is playing. */
61512
- durationMs: number().int().nonnegative().nullable(),
61943
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61513
61944
  /** Shuffle toggle. Null when no shuffle surface.
61514
61945
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61515
61946
  shuffle: boolean().nullable(),
@@ -61818,7 +62249,7 @@ var motionCustomize = {
61818
62249
  eventName: "onMotionChanged"
61819
62250
  },
61820
62251
  deviceTypes: [DeviceType.Camera],
61821
- apply: "replace",
62252
+ apply: "add",
61822
62253
  applyAuthority: {
61823
62254
  kind: "camera-sensors",
61824
62255
  timeoutField: "motionCooldownMs"
@@ -61890,7 +62321,7 @@ var MotionStatusSchema = object({
61890
62321
  * it, every provider seeded it at boot, and every session left it
61891
62322
  * `detected: true` after its detach.
61892
62323
  */
61893
- autoClearAfterMs: number().nullable()
62324
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61894
62325
  });
61895
62326
  /**
61896
62327
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61927,12 +62358,18 @@ var motionCapability = {
61927
62358
  methods: {
61928
62359
  /**
61929
62360
  * Pull the current motion state synchronously. Convenience shortcut
61930
- * for consumers that don't need the full status object; equivalent
61931
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61932
- * `getStatus` once the auto-injected status surface lands in every
61933
- * consumer.
62361
+ * for consumers that don't need the full status object. A composed
62362
+ * group answers from its slice (D944): unknown throws, it is not off.
61934
62363
  */
61935
62364
  isDetected: method(object({ deviceId: number() }), boolean()) },
62365
+ /**
62366
+ * A composed motion group serves `isDetected` from `detected` (D944).
62367
+ * Without this the cap stays uncomposable (`has-commands`).
62368
+ */
62369
+ composedMethods: { isDetected: {
62370
+ kind: "slice",
62371
+ field: "detected"
62372
+ } },
61936
62373
  events: {
61937
62374
  /**
61938
62375
  * Fires every time the runner transitions a camera between
@@ -62072,7 +62509,7 @@ var MotionZoneRegionSchema = object({
62072
62509
  * the grid region(s). */
62073
62510
  var MotionZoneStatusSchema = object({
62074
62511
  enabled: boolean(),
62075
- sensitivity: number(),
62512
+ sensitivity: number().meta(readingMeta("none")),
62076
62513
  /** Grid region(s). Today exactly one `grid` shape. */
62077
62514
  regions: array(MotionZoneRegionSchema),
62078
62515
  lastFetchedAt: number()
@@ -62442,7 +62879,7 @@ var NavigationStatusSchema = object({
62442
62879
  following: boolean(),
62443
62880
  flash: boolean(),
62444
62881
  lightMode: NavigationLightModeSchema,
62445
- lightLevel: number().min(40).max(100),
62882
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62446
62883
  /** Ms epoch when the slice was last updated. */
62447
62884
  lastChangedAt: number()
62448
62885
  });
@@ -62576,13 +63013,13 @@ var NetworkLinkStatusSchema = object({
62576
63013
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62577
63014
  * SKIP a null rather than coerce it.
62578
63015
  */
62579
- signalPercent: number().min(0).max(100).nullable(),
63016
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62580
63017
  /** Raw received signal strength in dBm, when the firmware reports one. */
62581
- rssiDbm: number().optional(),
63018
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62582
63019
  /** Network name of a wireless link, when the firmware reports it. */
62583
63020
  ssid: string().optional(),
62584
63021
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62585
- lastUpdated: number()
63022
+ lastUpdated: number().meta(readingMeta("none"))
62586
63023
  });
62587
63024
  var networkLinkCapability = {
62588
63025
  name: "network-link",
@@ -62967,7 +63404,7 @@ var NotifierStatusSchema = object({
62967
63404
  lastError: string().nullable(),
62968
63405
  /** Number of deliveries currently buffered server-side (rate-limited
62969
63406
  * notifiers may queue). 0 when the provider sends synchronously. */
62970
- queueDepth: number().int().nonnegative(),
63407
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62971
63408
  /** Per-feature capability matrix — authoritative source for the
62972
63409
  * compose-form field gating. */
62973
63410
  supports: NotifierSupportsSchema
@@ -63059,11 +63496,9 @@ onSent: { data: object({
63059
63496
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63060
63497
  * for: water flow, distance, weight, frequency, signal strength, …).
63061
63498
  *
63062
- * The `unit` and `precision` fields carry the live display metadata that
63063
- * the upstream source provides with each state push (HA
63064
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63065
- * The UI reads them directly from the slice — they are the single source
63066
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63499
+ * The quantity is not on this slice. It is chosen when the device is
63500
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63501
+ * that quantity's base unit. `null` is a refused sample (D393).
63067
63502
  *
63068
63503
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63069
63504
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63072,15 +63507,8 @@ onSent: { data: object({
63072
63507
  * numeric values land here.
63073
63508
  */
63074
63509
  var NumericSensorStatusSchema = object({
63075
- value: number(),
63076
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63077
- * Populated live from the upstream source on each state push.
63078
- * Absent for unitless sensors. */
63079
- unit: string().optional(),
63080
- /** Suggested decimal places for numeric display.
63081
- * Populated live from the upstream source when provided.
63082
- * Falls back to auto-formatting when absent. */
63083
- precision: number().int().min(0).max(10).optional(),
63510
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63511
+ value: number().nullable().meta(readingMeta("device")),
63084
63512
  /** Ms epoch when the slice was last updated. */
63085
63513
  lastFetchedAt: number()
63086
63514
  });
@@ -63104,11 +63532,11 @@ var numericSensorCapability = {
63104
63532
  */
63105
63533
  durability: "restored",
63106
63534
  /**
63107
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63535
+ * History (D850): recorded series. The unit is the device quantity's base,
63536
+ * resolved when the series is read — the cap has no single unit.
63108
63537
  */
63109
63538
  history: { fields: { value: {
63110
63539
  kind: "gauge",
63111
- unitFrom: "unit",
63112
63540
  minIntervalMs: 3e4
63113
63541
  } } },
63114
63542
  /** Clock fields: written, but excluded from the compare that decides
@@ -63400,23 +63828,26 @@ var PetFeederStatusSchema = object({
63400
63828
  /** Food currently in the bowl (grams). Null when the device has not
63401
63829
  * reported a reading yet. On dual-hopper models this is the combined
63402
63830
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63403
- foodLevel: number().nullable(),
63831
+ foodLevel: number().nullable().meta(readingMeta("none")),
63404
63832
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63405
63833
  * single-hopper models. */
63406
- food1: number().nullable(),
63834
+ food1: number().nullable().meta(readingMeta("none")),
63407
63835
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63408
63836
  * single-hopper models. */
63409
- food2: number().nullable(),
63837
+ food2: number().nullable().meta(readingMeta("none")),
63410
63838
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63411
63839
  * (`device_class: problem`, on = low). True when the bowl is empty /
63412
63840
  * below the feeder's low threshold. */
63413
63841
  lowFood: boolean(),
63414
63842
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63415
63843
  * device has no battery reading. */
63416
- batteryPower: number().min(0).max(100).nullable(),
63417
- /** Days of desiccant life remaining. Null when the model has no
63418
- * desiccant sensor. */
63419
- desiccantLeftDays: number().nullable(),
63844
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63845
+ /**
63846
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63847
+ * Providers convert from the days the feeder reports. Null when the
63848
+ * model has no desiccant sensor.
63849
+ */
63850
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63420
63851
  /** True while a feed is in progress. */
63421
63852
  feeding: boolean(),
63422
63853
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63427,7 +63858,7 @@ var PetFeederStatusSchema = object({
63427
63858
  * with `errorCode` for consumers that want the raw integer. */
63428
63859
  error: string().nullable(),
63429
63860
  /** Raw device error code (0 / null = no error). */
63430
- errorCode: number().nullable(),
63861
+ errorCode: number().nullable().meta(readingMeta("none")),
63431
63862
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63432
63863
  isDualHopper: boolean(),
63433
63864
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63437,7 +63868,7 @@ var PetFeederStatusSchema = object({
63437
63868
  /** Play a chime when dispensing. */
63438
63869
  feedSound: boolean(),
63439
63870
  /** Speaker / prompt volume level (device-scaled integer). */
63440
- volume: number(),
63871
+ volume: number().meta(readingMeta("none")),
63441
63872
  /** Ms epoch when the slice was last refreshed from the cloud. */
63442
63873
  lastFetchedAt: number()
63443
63874
  });
@@ -63556,7 +63987,7 @@ var petFeederCapability = {
63556
63987
  },
63557
63988
  desiccantLeftDays: {
63558
63989
  kind: "gauge",
63559
- unit: "days"
63990
+ unit: "s"
63560
63991
  }
63561
63992
  } }
63562
63993
  };
@@ -63957,36 +64388,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63957
64388
  */
63958
64389
  var PowerMeterStatusSchema = object({
63959
64390
  /** Instantaneous power draw in watts. */
63960
- watts: number().optional().meta({
63961
- unit: "W",
63962
- label: "Power"
63963
- }),
64391
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63964
64392
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63965
- kwhTotal: number().optional().meta({
63966
- unit: "kWh",
63967
- label: "Energy"
63968
- }),
64393
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63969
64394
  /** Voltage in volts. */
63970
- volts: number().optional(),
64395
+ volts: number().optional().meta(readingMeta("voltage")),
63971
64396
  /** Current in amperes. */
63972
- amps: number().optional(),
63973
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63974
- va: number().optional(),
64397
+ amps: number().optional().meta(readingMeta("current")),
64398
+ /** Apparent power in volt-amperes. */
64399
+ va: number().optional().meta(readingMeta("apparent-power")),
63975
64400
  /** Power factor (0..1) if reported. */
63976
- powerFactor: number().min(0).max(1).optional(),
64401
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63977
64402
  /** Ms epoch when the slice was last updated. */
63978
- lastFetchedAt: number(),
63979
- /** Live display unit of the single metric this slice carries (e.g. HA
63980
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63981
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63982
- * unit per slice is unambiguous. The UI prefers this over the role's
63983
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63984
- unit: string().optional(),
63985
- /** Suggested decimal places for numeric display.
63986
- * Populated live from the upstream source when provided (e.g. HA
63987
- * `attributes.suggested_display_precision`). Falls back to
63988
- * auto-formatting when absent. */
63989
- precision: number().int().min(0).max(10).optional()
64403
+ lastFetchedAt: number()
63990
64404
  });
63991
64405
  var powerMeterCapability = {
63992
64406
  name: "power-meter",
@@ -64050,12 +64464,7 @@ var powerMeterCapability = {
64050
64464
  } },
64051
64465
  /** Clock fields: written, but excluded from the compare that decides
64052
64466
  * whether persisting is worth a SQLite commit. */
64053
- volatileStateFields: ["lastFetchedAt"],
64054
- customize: {
64055
- version: 1,
64056
- advanced: "derive",
64057
- targets: []
64058
- }
64467
+ volatileStateFields: ["lastFetchedAt"]
64059
64468
  };
64060
64469
  /**
64061
64470
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64075,11 +64484,11 @@ var powerMeterCapability = {
64075
64484
  */
64076
64485
  var GpsLocationSchema = object({
64077
64486
  /** Latitude in decimal degrees, -90..90. */
64078
- latitude: number().min(-90).max(90),
64487
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64079
64488
  /** Longitude in decimal degrees, -180..180. */
64080
- longitude: number().min(-180).max(180),
64489
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64081
64490
  /** Reported accuracy in meters (lower = better). */
64082
- accuracyMeters: number().nonnegative()
64491
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64083
64492
  });
64084
64493
  var PresenceStatusSchema = object({
64085
64494
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64091,7 +64500,7 @@ var PresenceStatusSchema = object({
64091
64500
  gps: GpsLocationSchema.nullable(),
64092
64501
  /** Optional battery level of the tracking device (0..100). Null
64093
64502
  * when not reported. */
64094
- batteryPercent: number().min(0).max(100).nullable(),
64503
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64095
64504
  /** Ms epoch when the slice was last updated. */
64096
64505
  lastChangedAt: number()
64097
64506
  });
@@ -64141,19 +64550,10 @@ var presenceCapability = {
64141
64550
  * `sourceInfo`.
64142
64551
  */
64143
64552
  var PressureSensorStatusSchema = object({
64144
- /** Current pressure in hPa. */
64145
- hpa: number(),
64553
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64554
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64146
64555
  /** Ms epoch when the slice was last updated. */
64147
- lastFetchedAt: number(),
64148
- /** Live display unit from the upstream source (e.g. HA
64149
- * `attributes.unit_of_measurement`). The UI prefers this over the
64150
- * role's canonical unit. Absent → fall back to the canonical unit. */
64151
- unit: string().optional(),
64152
- /** Suggested decimal places for numeric display.
64153
- * Populated live from the upstream source when provided (e.g. HA
64154
- * `attributes.suggested_display_precision`). Falls back to
64155
- * auto-formatting when absent. */
64156
- precision: number().int().min(0).max(10).optional()
64556
+ lastFetchedAt: number()
64157
64557
  });
64158
64558
  var pressureSensorCapability = {
64159
64559
  name: "pressure-sensor",
@@ -64567,8 +64967,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64567
64967
  }), _void(), { kind: "mutation" });
64568
64968
  var PtzAutotrackSettingsSchema = object({
64569
64969
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64570
- stopDelaySeconds: number().int().min(0).max(300),
64571
- disappearDelaySeconds: number().int().min(0).max(300)
64970
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64971
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64572
64972
  });
64573
64973
  /**
64574
64974
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64722,7 +65122,7 @@ var RunningStateStatusSchema = object({
64722
65122
  /** null = unknown — never coerced to false (D393). */
64723
65123
  running: boolean().nullable(),
64724
65124
  /** Epoch ms the current boolean began; null while `running` is null. */
64725
- since: number().nullable(),
65125
+ since: number().nullable().meta(readingMeta("none")),
64726
65126
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64727
65127
  phase: string().nullable(),
64728
65128
  /** null until a reading has been published (D393). */
@@ -64737,7 +65137,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64737
65137
  /** Generic Customize target for the running boolean (D936). */
64738
65138
  var runningCustomize = {
64739
65139
  version: 1,
64740
- advanced: "derive",
64741
65140
  targets: [{
64742
65141
  id: "running",
64743
65142
  cardinality: "scalar",
@@ -64757,7 +65156,7 @@ var runningCustomize = {
64757
65156
  DeviceType.Thermostat,
64758
65157
  DeviceType.PetFeeder
64759
65158
  ],
64760
- apply: "add-or-replace",
65159
+ apply: "add",
64761
65160
  path: "running",
64762
65161
  sources: [
64763
65162
  {
@@ -66021,9 +66420,9 @@ var RecordTriggerSchema = _enum([
66021
66420
  */
66022
66421
  var RecordWindowSchema = object({
66023
66422
  trigger: RecordTriggerSchema,
66024
- day: number().int().min(0).max(6),
66025
- startMinute: number().int().min(0).max(1439),
66026
- endMinute: number().int().min(1).max(1440)
66423
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66424
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66425
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66027
66426
  });
66028
66427
  /** Status of one physical volume, as the camera itself describes it. */
66029
66428
  var OnboardStorageVolumeSchema = object({
@@ -66099,10 +66498,10 @@ var RecordingOnboardStatusSchema = object({
66099
66498
  /** Master "record to the card at all" switch. */
66100
66499
  enabled: boolean().nullable(),
66101
66500
  overwriteWhenFull: boolean().nullable(),
66102
- preRecordSec: number().nullable(),
66103
- postRecordSec: number().nullable(),
66501
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66502
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66104
66503
  /** Length of one recorded file, in minutes. */
66105
- segmentMinutes: number().nullable(),
66504
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66106
66505
  /** The primary track's weekly windows, flattened. */
66107
66506
  windows: array(RecordWindowSchema),
66108
66507
  /**
@@ -66118,7 +66517,7 @@ var RecordingOnboardStatusSchema = object({
66118
66517
  * provider reporting a non-zero count also reports the schedule as not
66119
66518
  * writable.
66120
66519
  */
66121
- unreadableWindows: number(),
66520
+ unreadableWindows: number().meta(readingMeta("count")),
66122
66521
  /**
66123
66522
  * The camera is scheduled to record and has NO usable storage.
66124
66523
  *
@@ -66963,7 +67362,7 @@ var CamStreamDescriptorSchema = object({
66963
67362
  url: string().optional(),
66964
67363
  codec: string().optional(),
66965
67364
  resolution: CamStreamResolutionSchema.optional(),
66966
- fps: number().positive().optional(),
67365
+ fps: number().positive().optional().meta(readingMeta("rate")),
66967
67366
  label: string().optional(),
66968
67367
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66969
67368
  deviceFeatures: array(string()).optional(),
@@ -67050,18 +67449,18 @@ var StreamProfileSchema = _enum([
67050
67449
  ]);
67051
67450
  /** The current encoding config of one stream profile. */
67052
67451
  var StreamProfileConfigSchema = object({
67053
- width: number(),
67054
- height: number(),
67452
+ width: number().meta(readingMeta("none")),
67453
+ height: number().meta(readingMeta("none")),
67055
67454
  codec: _enum(["h264", "h265"]),
67056
- framerate: number(),
67057
- bitrate: number(),
67455
+ framerate: number().meta(readingMeta("rate")),
67456
+ bitrate: number().meta(readingMeta("bitrate")),
67058
67457
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67059
67458
  encoderProfile: _enum([
67060
67459
  "high",
67061
67460
  "main",
67062
67461
  "baseline"
67063
67462
  ]).optional(),
67064
- gop: number().optional(),
67463
+ gop: number().optional().meta(readingMeta("none")),
67065
67464
  /**
67066
67465
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67067
67466
  *
@@ -67282,12 +67681,7 @@ var switchCapability = {
67282
67681
  label: "On",
67283
67682
  kind: "boolean"
67284
67683
  }
67285
- }] },
67286
- customize: {
67287
- version: 1,
67288
- advanced: "derive",
67289
- targets: []
67290
- }
67684
+ }] }
67291
67685
  };
67292
67686
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67293
67687
  function hasControlCharacter(value) {
@@ -67870,35 +68264,21 @@ var tamperCapability = {
67870
68264
  * entries with `device_class: temperature` and any future native
67871
68265
  * thermometer.
67872
68266
  *
67873
- * Unit normalisation: providers convert to Celsius before storing.
67874
- * The slice value is always Celsius so cross-cap aggregators
67875
- * (climate-control's `currentTemp`, energy analytics) can compose
67876
- * without per-source unit fixups. The canonical display unit (`°C`) is
67877
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67878
- * `sourceInfo`.
68267
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68268
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67879
68269
  *
67880
68270
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67881
68271
  * frozen feed (provider hung) distinct from a "temperature hasn't
67882
68272
  * changed" steady state.
67883
68273
  */
67884
68274
  var TemperatureSensorStatusSchema = object({
67885
- /** Current temperature in Celsius. */
67886
- celsius: number().meta({
67887
- unit: "°C",
68275
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68276
+ celsius: number().nullable().meta(readingMeta("temperature", {
68277
+ precision: 1,
67888
68278
  label: "Temperature"
67889
- }),
68279
+ })),
67890
68280
  /** Ms epoch when the slice was last updated (push or poll). */
67891
- lastFetchedAt: number(),
67892
- /** Live display unit from the upstream source (e.g. HA
67893
- * `attributes.unit_of_measurement`). The UI prefers this over the
67894
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67895
- * Absent → the UI falls back to the role's canonical unit. */
67896
- unit: string().optional(),
67897
- /** Suggested decimal places for numeric display.
67898
- * Populated live from the upstream source when provided (e.g. HA
67899
- * `attributes.suggested_display_precision`). Falls back to
67900
- * auto-formatting when absent. */
67901
- precision: number().int().min(0).max(10).optional()
68281
+ lastFetchedAt: number()
67902
68282
  });
67903
68283
  var temperatureSensorCapability = {
67904
68284
  name: "temperature-sensor",
@@ -67929,12 +68309,7 @@ var temperatureSensorCapability = {
67929
68309
  } } },
67930
68310
  /** Clock fields: written, but excluded from the compare that decides
67931
68311
  * whether persisting is worth a SQLite commit. */
67932
- volatileStateFields: ["lastFetchedAt"],
67933
- customize: {
67934
- version: 1,
67935
- advanced: "derive",
67936
- targets: []
67937
- }
68312
+ volatileStateFields: ["lastFetchedAt"]
67938
68313
  };
67939
68314
  /**
67940
68315
  * toast — system-scoped singleton capability that streams toast
@@ -68217,16 +68592,16 @@ var VacuumControlStatusSchema = object({
68217
68592
  ]),
68218
68593
  /** 0..100 battery percentage. Null when the device has no battery
68219
68594
  * reading. */
68220
- batteryLevel: number().min(0).max(100).nullable(),
68595
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68221
68596
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68222
68597
  * the vacuum has no speed control. */
68223
68598
  fanSpeed: string().nullable(),
68224
68599
  /** Speed tokens the hardware accepts — drives the UI selector. */
68225
68600
  availableFanSpeeds: array(string()),
68226
68601
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68227
- progressPercent: number().min(0).max(100).nullable(),
68602
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68228
68603
  /** Current error code (0 / null = no error). */
68229
- errorCode: number().nullable(),
68604
+ errorCode: number().nullable().meta(readingMeta("none")),
68230
68605
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68231
68606
  errorLabel: string().nullable(),
68232
68607
  /** Ms epoch when the slice was last updated. */
@@ -68304,7 +68679,7 @@ var ValveStatusSchema = object({
68304
68679
  ]),
68305
68680
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68306
68681
  * intermediate position surface. */
68307
- position: number().min(0).max(100).nullable(),
68682
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68308
68683
  /** Ms epoch when the slice was last updated. */
68309
68684
  lastChangedAt: number()
68310
68685
  });
@@ -68412,9 +68787,9 @@ var vibrationCapability = {
68412
68787
  */
68413
68788
  var WaterHeaterStatusSchema = object({
68414
68789
  /** Current measured temperature. Null when not reported. */
68415
- currentTemp: number().nullable(),
68790
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68416
68791
  /** Target temperature setpoint. Null when no setpoint surface. */
68417
- targetTemp: number().nullable(),
68792
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68418
68793
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68419
68794
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68420
68795
  * device reports an unknown state. */
@@ -68425,9 +68800,9 @@ var WaterHeaterStatusSchema = object({
68425
68800
  * has no away surface. */
68426
68801
  away: boolean().nullable(),
68427
68802
  /** HA `min_temp` attribute. Null when not reported. */
68428
- minTemp: number().nullable(),
68803
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68429
68804
  /** HA `max_temp` attribute. Null when not reported. */
68430
- maxTemp: number().nullable(),
68805
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68431
68806
  /** Ms epoch when the slice was last updated. */
68432
68807
  lastChangedAt: number()
68433
68808
  });
@@ -68512,25 +68887,18 @@ var waterHeaterCapability = {
68512
68887
  * Forecast deferred — current conditions only for v1.
68513
68888
  */
68514
68889
  var WeatherStatusSchema = object({
68515
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68516
- * when no condition has been reported yet. */
68890
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68517
68891
  condition: string().nullable(),
68518
- /** Current temperature in the reported unit. Null when not provided. */
68519
- temperature: number().nullable(),
68520
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68521
- temperatureUnit: string().nullable(),
68892
+ /** Current temperature in Celsius. Null when not provided or refused. */
68893
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68522
68894
  /** Relative humidity (0..100). Null when not provided. */
68523
- humidity: number().min(0).max(100).nullable(),
68524
- /** Barometric pressure in the reported unit. Null when not provided. */
68525
- pressure: number().nullable(),
68526
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68527
- pressureUnit: string().nullable(),
68528
- /** Wind speed in the reported unit. Null when not provided. */
68529
- windSpeed: number().nullable(),
68530
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68531
- windSpeedUnit: string().nullable(),
68895
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68896
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68897
+ pressure: number().nullable().meta(readingMeta("pressure")),
68898
+ /** Wind speed in metres per second. Null when not provided or refused. */
68899
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68532
68900
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68533
- windBearing: number().nullable(),
68901
+ windBearing: number().nullable().meta(readingMeta("angle")),
68534
68902
  /** Ms epoch when the slice was last updated. */
68535
68903
  lastFetchedAt: number()
68536
68904
  });
@@ -68558,12 +68926,12 @@ var weatherCapability = {
68558
68926
  */
68559
68927
  durability: "session",
68560
68928
  /**
68561
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68929
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68562
68930
  */
68563
68931
  history: { fields: {
68564
68932
  temperature: {
68565
68933
  kind: "gauge",
68566
- unitFrom: "temperatureUnit",
68934
+ unit: "°C",
68567
68935
  deadband: .2
68568
68936
  },
68569
68937
  humidity: {
@@ -68573,12 +68941,12 @@ var weatherCapability = {
68573
68941
  },
68574
68942
  pressure: {
68575
68943
  kind: "gauge",
68576
- unitFrom: "pressureUnit",
68944
+ unit: "hPa",
68577
68945
  deadband: .5
68578
68946
  },
68579
68947
  windSpeed: {
68580
68948
  kind: "gauge",
68581
- unitFrom: "windSpeedUnit",
68949
+ unit: "m/s",
68582
68950
  deadband: .5
68583
68951
  }
68584
68952
  } }
@@ -69809,6 +70177,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69809
70177
  featureProbe: featureProbeCapability,
69810
70178
  flood: floodCapability,
69811
70179
  gas: gasCapability,
70180
+ groupCommands: groupCommandsCapability,
70181
+ groupStatus: groupStatusCapability,
69812
70182
  heatingValve: heatingValveCapability,
69813
70183
  humidifier: humidifierCapability,
69814
70184
  humiditySensor: humiditySensorCapability,
@@ -71185,6 +71555,13 @@ Object.freeze({
71185
71555
  access: "view",
71186
71556
  role: "admin"
71187
71557
  },
71558
+ "addons.getPackageDocs": {
71559
+ capName: "addons",
71560
+ capScope: "system",
71561
+ addonId: null,
71562
+ access: "view",
71563
+ role: "admin"
71564
+ },
71188
71565
  "addons.getVersions": {
71189
71566
  capName: "addons",
71190
71567
  capScope: "system",
@@ -74293,6 +74670,20 @@ Object.freeze({
74293
74670
  access: "view",
74294
74671
  role: "viewer"
74295
74672
  },
74673
+ "groupCommands.getStatus": {
74674
+ capName: "group-commands",
74675
+ capScope: "device",
74676
+ addonId: null,
74677
+ access: "view",
74678
+ role: "viewer"
74679
+ },
74680
+ "groupStatus.getStatus": {
74681
+ capName: "group-status",
74682
+ capScope: "device",
74683
+ addonId: null,
74684
+ access: "view",
74685
+ role: "viewer"
74686
+ },
74296
74687
  "heatingValve.getStatus": {
74297
74688
  capName: "heating-valve",
74298
74689
  capScope: "device",
@@ -84115,6 +84506,37 @@ var SENSOR_CAP_VOCABULARY = {
84115
84506
  icon: "flame"
84116
84507
  }]
84117
84508
  },
84509
+ "group-commands": {
84510
+ scope: "device",
84511
+ noun: t("Group command", "Comando di gruppo")
84512
+ },
84513
+ "group-status": {
84514
+ scope: "device",
84515
+ noun: t("Group", "Gruppo"),
84516
+ state: [
84517
+ {
84518
+ kind: "numeric",
84519
+ field: "included",
84520
+ quantity: t("the members reporting", "i membri che rispondono"),
84521
+ label: t("Reporting", "Rispondono"),
84522
+ icon: "users"
84523
+ },
84524
+ {
84525
+ kind: "numeric",
84526
+ field: "excluded",
84527
+ quantity: t("the members left out", "i membri esclusi"),
84528
+ label: t("Excluded", "Esclusi"),
84529
+ icon: "users"
84530
+ },
84531
+ {
84532
+ kind: "numeric",
84533
+ field: "total",
84534
+ quantity: t("the member count", "il numero di membri"),
84535
+ label: t("Members", "Membri"),
84536
+ icon: "sigma"
84537
+ }
84538
+ ]
84539
+ },
84118
84540
  "humidity-sensor": {
84119
84541
  scope: "device",
84120
84542
  noun: t("Humidity", "Umidità"),
@@ -84148,7 +84570,6 @@ var SENSOR_CAP_VOCABULARY = {
84148
84570
  field: "value",
84149
84571
  quantity: t("the reading", "il valore"),
84150
84572
  label: t("Reading", "Valore"),
84151
- unitField: "unit",
84152
84573
  icon: "gauge"
84153
84574
  }]
84154
84575
  },
@@ -84284,7 +84705,7 @@ var SENSOR_CAP_VOCABULARY = {
84284
84705
  field: "temperature",
84285
84706
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84286
84707
  label: t("Temperature", "Temperatura"),
84287
- unitField: "temperatureUnit",
84708
+ unit: "°C",
84288
84709
  icon: "thermometer"
84289
84710
  },
84290
84711
  {
@@ -84300,7 +84721,7 @@ var SENSOR_CAP_VOCABULARY = {
84300
84721
  field: "windSpeed",
84301
84722
  quantity: t("the wind speed", "la velocità del vento"),
84302
84723
  label: t("Wind", "Vento"),
84303
- unitField: "windSpeedUnit",
84724
+ unit: "m/s",
84304
84725
  icon: "wind"
84305
84726
  },
84306
84727
  {
@@ -84308,7 +84729,7 @@ var SENSOR_CAP_VOCABULARY = {
84308
84729
  field: "pressure",
84309
84730
  quantity: t("the air pressure", "la pressione atmosferica"),
84310
84731
  label: t("Pressure", "Pressione"),
84311
- unitField: "pressureUnit",
84732
+ unit: "hPa",
84312
84733
  icon: "gauge"
84313
84734
  }
84314
84735
  ]
@@ -85581,6 +86002,16 @@ Object.freeze({
85581
86002
  form: "single",
85582
86003
  optional: false
85583
86004
  }],
86005
+ "groupCommands.getStatus": [{
86006
+ name: "deviceId",
86007
+ form: "single",
86008
+ optional: false
86009
+ }],
86010
+ "groupStatus.getStatus": [{
86011
+ name: "deviceId",
86012
+ form: "single",
86013
+ optional: false
86014
+ }],
85584
86015
  "heatingValve.getStatus": [{
85585
86016
  name: "deviceId",
85586
86017
  form: "single",
@@ -87565,6 +87996,8 @@ new Map(Object.entries({
87565
87996
  "detection-blind": "device-events",
87566
87997
  "motion-noisy": "device-events",
87567
87998
  "motion-calm": "device-events",
87999
+ "package-llm-silent": "device-events",
88000
+ "package-llm-answered": "device-events",
87568
88001
  "recording-stalled": "device-events",
87569
88002
  "recording-healthy": "device-events",
87570
88003
  "recording-degraded": "device-events",
@@ -87648,6 +88081,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87648
88081
  label: "Motion without a subject",
87649
88082
  kinds: ["motion-noisy", "motion-calm"]
87650
88083
  },
88084
+ {
88085
+ id: "package-llm",
88086
+ family: "device-events",
88087
+ label: "Package confirm",
88088
+ kinds: ["package-llm-silent", "package-llm-answered"]
88089
+ },
87651
88090
  {
87652
88091
  id: "recording",
87653
88092
  family: "device-events",
@@ -87829,6 +88268,8 @@ var DEVICE_EVENTS = [
87829
88268
  "detection-blind",
87830
88269
  "motion-noisy",
87831
88270
  "motion-calm",
88271
+ "package-llm-silent",
88272
+ "package-llm-answered",
87832
88273
  "recording-stalled",
87833
88274
  "recording-healthy",
87834
88275
  "recording-degraded",
@@ -88097,6 +88538,15 @@ new Map([
88097
88538
  snapshotLabel: "Cameras with constant motion now",
88098
88539
  snapshotSection: null
88099
88540
  },
88541
+ {
88542
+ id: "package-llm-confirm",
88543
+ family: "device-events",
88544
+ label: "Package confirm (not answering / answering again)",
88545
+ bad: ["package-llm-silent"],
88546
+ recovery: ["package-llm-answered"],
88547
+ snapshotLabel: "Cameras whose package confirm is not answering",
88548
+ snapshotSection: null
88549
+ },
88100
88550
  {
88101
88551
  id: "recording-health",
88102
88552
  family: "device-events",