@camstack/system 1.2.484 → 1.2.485

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.
Files changed (87) hide show
  1. package/dist/{agent-https-CW8aLMDL.js → agent-https-DBP7P-ee.js} +1 -1
  2. package/dist/{agent-https-DttPIFry.mjs → agent-https-_EXvi6F8.mjs} +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/calendars/calendars.addon.js +1 -1
  14. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  15. package/dist/builtins/camera-grid/index.js +1 -1
  16. package/dist/builtins/camera-grid/index.mjs +1 -1
  17. package/dist/builtins/composer/composer.addon.js +13 -5
  18. package/dist/builtins/composer/composer.addon.mjs +13 -5
  19. package/dist/builtins/console-logging/index.js +1 -1
  20. package/dist/builtins/console-logging/index.mjs +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  22. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  23. package/dist/builtins/device-history/device-history.addon.d.ts +2 -0
  24. package/dist/builtins/device-history/device-history.addon.js +248 -4
  25. package/dist/builtins/device-history/device-history.addon.mjs +248 -4
  26. package/dist/builtins/device-history/query/history-query.d.ts +5 -1
  27. package/dist/builtins/device-history/runtime/history-runtime.d.ts +18 -1
  28. package/dist/builtins/device-history/store/history-schema.d.ts +2 -0
  29. package/dist/builtins/device-history/store/history-store.d.ts +11 -0
  30. package/dist/builtins/device-history/store/series-scale.d.ts +37 -0
  31. package/dist/builtins/device-manager/device-manager.addon.js +3 -2
  32. package/dist/builtins/device-manager/device-manager.addon.mjs +3 -2
  33. package/dist/builtins/hub-forwarder/index.js +1 -1
  34. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  35. package/dist/builtins/keychain/index.js +3 -3
  36. package/dist/builtins/keychain/index.mjs +3 -3
  37. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  38. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  39. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  40. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  41. package/dist/builtins/local-network/local-network.addon.js +2 -2
  42. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  43. package/dist/builtins/loki-logging/index.js +1 -1
  44. package/dist/builtins/loki-logging/index.mjs +1 -1
  45. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  46. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  47. package/dist/builtins/platform-probe/index.js +1 -1
  48. package/dist/builtins/platform-probe/index.mjs +1 -1
  49. package/dist/builtins/record-entries/index.js +1 -1
  50. package/dist/builtins/record-entries/index.mjs +1 -1
  51. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  53. package/dist/builtins/snapshot/index.js +1 -1
  54. package/dist/builtins/snapshot/index.mjs +1 -1
  55. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  56. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  57. package/dist/builtins/sqlite-storage/index.js +1 -1
  58. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  60. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  62. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  63. package/dist/builtins/system-config/system-config.addon.js +3 -3
  64. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  65. package/dist/builtins/winston-logging/index.js +1 -1
  66. package/dist/builtins/winston-logging/index.mjs +1 -1
  67. package/dist/{composition-sources-DqRPHdtw.js → composition-sources-BSQneZV9.js} +1 -1
  68. package/dist/{composition-sources-DEsDonpg.mjs → composition-sources-BnZpIA8u.mjs} +1 -1
  69. package/dist/{constants-B3FDrBIq.mjs → constants-7D7xjw0h.mjs} +1 -1
  70. package/dist/{constants-71vf77HY.js → constants-CQfTeCnB.js} +1 -1
  71. package/dist/{device-ca-bhz04X_N.mjs → device-ca-Bzd6UO_c.mjs} +1 -1
  72. package/dist/{device-ca-BDPYXhkH.js → device-ca-D_4dI5gv.js} +1 -1
  73. package/dist/{dist-BT_THdzJ.mjs → dist-Df6c0RX_.mjs} +607 -522
  74. package/dist/{dist-k--OhKkO.js → dist-xCNJAQMh.js} +618 -521
  75. package/dist/file-host-worker.js +1 -1
  76. package/dist/file-host-worker.mjs +1 -1
  77. package/dist/{hub-mdns-ciao-DDgdvcQw.mjs → hub-mdns-ciao-D7GMnEz0.mjs} +2 -2
  78. package/dist/{hub-mdns-ciao-CGXfhvTk.js → hub-mdns-ciao-DRfmLR_Z.js} +2 -2
  79. package/dist/index.js +7 -7
  80. package/dist/index.mjs +7 -7
  81. package/dist/{restore-archive-z0XT2g29.mjs → restore-archive-Co70oRya.mjs} +1 -1
  82. package/dist/{restore-archive-B-GiglWt.js → restore-archive-DA7N6ZZA.js} +1 -1
  83. package/dist/{retention-DR0_xRTy.js → retention-DNDOPW7Z.js} +1 -1
  84. package/dist/{retention-DFkmhjeH.mjs → retention-DfCW-dZS.mjs} +1 -1
  85. package/dist/{wal-maintenance-CVDrL_BD.mjs → wal-maintenance-71-j48Ci.mjs} +0 -0
  86. package/dist/{wal-maintenance-C4cma95b.js → wal-maintenance-CA0L6WZ9.js} +0 -0
  87. package/package.json +1 -1
@@ -765,7 +765,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
765
765
  return EventCategory;
766
766
  }({});
767
767
  //#endregion
768
- //#region ../types/dist/sleep-BErAxhul.mjs
768
+ //#region ../types/dist/sleep-CARd6wZF.mjs
769
769
  /**
770
770
  * The audio chunk plane's byte format, and the ONE expansion from a coded
771
771
  * window to float samples (D455).
@@ -3801,6 +3801,112 @@ function assertTransactScope(members) {
3801
3801
  if (missing.length > 0) throw new TransactRefusedError(`no-family: ${missing.map((m) => m.kind).join(", ")} declare no transactFamily; kinds that must agree through transact share one`);
3802
3802
  if (new Set(kinds.map((m) => m.declaration.transactFamily)).size > 1) throw new TransactRefusedError(`cross-family: the ops span more than one transactFamily (${kinds.map((m) => `${m.kind}=${String(m.declaration.transactFamily)}`).join(", ")})`);
3803
3803
  }
3804
+ var READING_QUANTITIES = [
3805
+ "temperature",
3806
+ "pressure",
3807
+ "speed",
3808
+ "precipitation",
3809
+ "precipitation-rate",
3810
+ "length",
3811
+ "area",
3812
+ "illuminance",
3813
+ "irradiance",
3814
+ "power",
3815
+ "energy",
3816
+ "voltage",
3817
+ "current",
3818
+ "apparent-power",
3819
+ "humidity",
3820
+ "percentage",
3821
+ "concentration-volume",
3822
+ "concentration-mass",
3823
+ "concentration-ppb",
3824
+ "volume",
3825
+ "duration",
3826
+ "duration-ms",
3827
+ "information",
3828
+ "sound-level",
3829
+ "signal-level",
3830
+ "colour-temperature",
3831
+ "angle",
3832
+ "bitrate",
3833
+ "rate",
3834
+ "mass",
3835
+ "index",
3836
+ "ratio",
3837
+ "count",
3838
+ "none",
3839
+ "device"
3840
+ ];
3841
+ var ReadingQuantitySchema = zod.z.enum(READING_QUANTITIES);
3842
+ var QUANTITY_SET = new Set(READING_QUANTITIES);
3843
+ function isReadingQuantity(value) {
3844
+ return typeof value === "string" && QUANTITY_SET.has(value);
3845
+ }
3846
+ /**
3847
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
3848
+ * (or an index / ratio / count). `device` means the cap has no single unit.
3849
+ */
3850
+ var QUANTITY_BASE = {
3851
+ temperature: "°C",
3852
+ pressure: "hPa",
3853
+ speed: "m/s",
3854
+ precipitation: "mm",
3855
+ "precipitation-rate": "mm/h",
3856
+ length: "m",
3857
+ area: "m²",
3858
+ illuminance: "lx",
3859
+ irradiance: "W/m²",
3860
+ power: "W",
3861
+ energy: "kWh",
3862
+ voltage: "V",
3863
+ current: "A",
3864
+ "apparent-power": "VA",
3865
+ humidity: "%",
3866
+ percentage: "%",
3867
+ "concentration-volume": "ppm",
3868
+ "concentration-mass": "µg/m³",
3869
+ "concentration-ppb": "ppb",
3870
+ volume: "L",
3871
+ duration: "s",
3872
+ "duration-ms": "ms",
3873
+ information: "B",
3874
+ "sound-level": "dBFS",
3875
+ "signal-level": "dBm",
3876
+ "colour-temperature": "mired",
3877
+ angle: "°",
3878
+ bitrate: "kbps",
3879
+ rate: "fps",
3880
+ mass: "kg",
3881
+ index: "none",
3882
+ ratio: "none",
3883
+ count: "none",
3884
+ none: "none",
3885
+ device: "device"
3886
+ };
3887
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
3888
+ function baseUnitOf(quantity) {
3889
+ const symbol = QUANTITY_BASE[quantity];
3890
+ if (symbol === "none" || symbol === "device") return void 0;
3891
+ return symbol;
3892
+ }
3893
+ /**
3894
+ * History catalog label for a quantity. Index and ratio keep the labels the
3895
+ * series already used (`count`, `ratio`). A polymorphic reading has no
3896
+ * single label.
3897
+ */
3898
+ function historyUnitFor(quantity) {
3899
+ if (quantity === "index" || quantity === "count") return "count";
3900
+ if (quantity === "ratio") return "ratio";
3901
+ return baseUnitOf(quantity);
3902
+ }
3903
+ function readingMeta(quantity, extras) {
3904
+ return {
3905
+ quantity,
3906
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
3907
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
3908
+ };
3909
+ }
3804
3910
  /** Shared Zod schemas used across streaming capabilities. */
3805
3911
  var CamProfileSchema = zod.z.enum([
3806
3912
  "high",
@@ -3828,8 +3934,8 @@ var CamStreamKindSchema = zod.z.enum([
3828
3934
  "derived"
3829
3935
  ]);
3830
3936
  var CamStreamResolutionSchema = zod.z.object({
3831
- width: zod.z.number().int().positive(),
3832
- height: zod.z.number().int().positive()
3937
+ width: zod.z.number().int().positive().meta(readingMeta("none")),
3938
+ height: zod.z.number().int().positive().meta(readingMeta("none"))
3833
3939
  });
3834
3940
  var CameraStreamSchema = zod.z.object({
3835
3941
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -5206,14 +5312,6 @@ Object.freeze({
5206
5312
  {
5207
5313
  "path": "aqi",
5208
5314
  "valueType": "number"
5209
- },
5210
- {
5211
- "path": "unit",
5212
- "valueType": "string"
5213
- },
5214
- {
5215
- "path": "precision",
5216
- "valueType": "number"
5217
5315
  }
5218
5316
  ],
5219
5317
  "alarm-panel": [
@@ -5249,20 +5347,10 @@ Object.freeze({
5249
5347
  "valueType": "boolean"
5250
5348
  }
5251
5349
  ],
5252
- "ambient-light-sensor": [
5253
- {
5254
- "path": "lux",
5255
- "valueType": "number"
5256
- },
5257
- {
5258
- "path": "unit",
5259
- "valueType": "string"
5260
- },
5261
- {
5262
- "path": "precision",
5263
- "valueType": "number"
5264
- }
5265
- ],
5350
+ "ambient-light-sensor": [{
5351
+ "path": "lux",
5352
+ "valueType": "number"
5353
+ }],
5266
5354
  "audio-metrics": [
5267
5355
  {
5268
5356
  "path": "ts",
@@ -5342,6 +5430,10 @@ Object.freeze({
5342
5430
  "path": "lastUpdated",
5343
5431
  "valueType": "number"
5344
5432
  },
5433
+ {
5434
+ "path": "volts",
5435
+ "valueType": "number"
5436
+ },
5345
5437
  {
5346
5438
  "path": "lastContactAt",
5347
5439
  "valueType": "number"
@@ -5584,10 +5676,6 @@ Object.freeze({
5584
5676
  "datetime"
5585
5677
  ]
5586
5678
  },
5587
- {
5588
- "path": "unit",
5589
- "valueType": "string"
5590
- },
5591
5679
  {
5592
5680
  "path": "min",
5593
5681
  "valueType": "number"
@@ -5599,19 +5687,6 @@ Object.freeze({
5599
5687
  {
5600
5688
  "path": "step",
5601
5689
  "valueType": "number"
5602
- },
5603
- {
5604
- "path": "precision",
5605
- "valueType": "number"
5606
- },
5607
- {
5608
- "path": "format",
5609
- "valueType": "enum",
5610
- "enumValues": [
5611
- "date",
5612
- "time",
5613
- "datetime"
5614
- ]
5615
5690
  }
5616
5691
  ],
5617
5692
  "cover": [
@@ -5852,13 +5927,8 @@ Object.freeze({
5852
5927
  "path": "value",
5853
5928
  "valueType": "string"
5854
5929
  }, {
5855
- "path": "format",
5856
- "valueType": "enum",
5857
- "enumValues": [
5858
- "date",
5859
- "time",
5860
- "datetime"
5861
- ]
5930
+ "path": "timestamp",
5931
+ "valueType": "boolean"
5862
5932
  }],
5863
5933
  "event-emitter": [
5864
5934
  {
@@ -5965,20 +6035,10 @@ Object.freeze({
5965
6035
  "valueType": "number"
5966
6036
  }
5967
6037
  ],
5968
- "humidity-sensor": [
5969
- {
5970
- "path": "percent",
5971
- "valueType": "number"
5972
- },
5973
- {
5974
- "path": "unit",
5975
- "valueType": "string"
5976
- },
5977
- {
5978
- "path": "precision",
5979
- "valueType": "number"
5980
- }
5981
- ],
6038
+ "humidity-sensor": [{
6039
+ "path": "percent",
6040
+ "valueType": "number"
6041
+ }],
5982
6042
  "image": [{
5983
6043
  "path": "url",
5984
6044
  "valueType": "string"
@@ -6317,20 +6377,10 @@ Object.freeze({
6317
6377
  "valueType": "boolean"
6318
6378
  }
6319
6379
  ],
6320
- "numeric-sensor": [
6321
- {
6322
- "path": "value",
6323
- "valueType": "number"
6324
- },
6325
- {
6326
- "path": "unit",
6327
- "valueType": "string"
6328
- },
6329
- {
6330
- "path": "precision",
6331
- "valueType": "number"
6332
- }
6333
- ],
6380
+ "numeric-sensor": [{
6381
+ "path": "value",
6382
+ "valueType": "number"
6383
+ }],
6334
6384
  "pet-feeder": [
6335
6385
  {
6336
6386
  "path": "foodLevel",
@@ -6422,14 +6472,6 @@ Object.freeze({
6422
6472
  {
6423
6473
  "path": "powerFactor",
6424
6474
  "valueType": "number"
6425
- },
6426
- {
6427
- "path": "unit",
6428
- "valueType": "string"
6429
- },
6430
- {
6431
- "path": "precision",
6432
- "valueType": "number"
6433
6475
  }
6434
6476
  ],
6435
6477
  "presence": [
@@ -6458,20 +6500,10 @@ Object.freeze({
6458
6500
  "valueType": "number"
6459
6501
  }
6460
6502
  ],
6461
- "pressure-sensor": [
6462
- {
6463
- "path": "hpa",
6464
- "valueType": "number"
6465
- },
6466
- {
6467
- "path": "unit",
6468
- "valueType": "string"
6469
- },
6470
- {
6471
- "path": "precision",
6472
- "valueType": "number"
6473
- }
6474
- ],
6503
+ "pressure-sensor": [{
6504
+ "path": "hpa",
6505
+ "valueType": "number"
6506
+ }],
6475
6507
  "privacy-mask": [{
6476
6508
  "path": "enabled",
6477
6509
  "valueType": "boolean"
@@ -6782,20 +6814,10 @@ Object.freeze({
6782
6814
  "path": "tampered",
6783
6815
  "valueType": "boolean"
6784
6816
  }],
6785
- "temperature-sensor": [
6786
- {
6787
- "path": "celsius",
6788
- "valueType": "number"
6789
- },
6790
- {
6791
- "path": "unit",
6792
- "valueType": "string"
6793
- },
6794
- {
6795
- "path": "precision",
6796
- "valueType": "number"
6797
- }
6798
- ],
6817
+ "temperature-sensor": [{
6818
+ "path": "celsius",
6819
+ "valueType": "number"
6820
+ }],
6799
6821
  "update": [
6800
6822
  {
6801
6823
  "path": "currentVersion",
@@ -6906,10 +6928,6 @@ Object.freeze({
6906
6928
  "path": "temperature",
6907
6929
  "valueType": "number"
6908
6930
  },
6909
- {
6910
- "path": "temperatureUnit",
6911
- "valueType": "string"
6912
- },
6913
6931
  {
6914
6932
  "path": "humidity",
6915
6933
  "valueType": "number"
@@ -6918,18 +6936,10 @@ Object.freeze({
6918
6936
  "path": "pressure",
6919
6937
  "valueType": "number"
6920
6938
  },
6921
- {
6922
- "path": "pressureUnit",
6923
- "valueType": "string"
6924
- },
6925
6939
  {
6926
6940
  "path": "windSpeed",
6927
6941
  "valueType": "number"
6928
6942
  },
6929
- {
6930
- "path": "windSpeedUnit",
6931
- "valueType": "string"
6932
- },
6933
6943
  {
6934
6944
  "path": "windBearing",
6935
6945
  "valueType": "number"
@@ -17134,6 +17144,17 @@ function unwrapSchema(schema) {
17134
17144
  while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
17135
17145
  return s;
17136
17146
  }
17147
+ function isMetaBag(value) {
17148
+ return typeof value === "object" && value !== null;
17149
+ }
17150
+ function metaQuantity(schema) {
17151
+ const bag = zod.z.globalRegistry.get(schema);
17152
+ if (isMetaBag(bag) && isReadingQuantity(bag.quantity)) return bag.quantity;
17153
+ }
17154
+ /** Quantity declared with `readingMeta` on the field or on the unwrapped leaf. */
17155
+ function quantityOfField(schema) {
17156
+ return metaQuantity(schema) ?? metaQuantity(unwrapSchema(schema));
17157
+ }
17137
17158
  function leafKind(schema) {
17138
17159
  const s = unwrapSchema(schema);
17139
17160
  if (s instanceof zod.z.ZodEnum) return {
@@ -17162,11 +17183,15 @@ function enumerateSchemaFields(schema, prefix = "") {
17162
17183
  continue;
17163
17184
  }
17164
17185
  const leaf = leafKind(field);
17165
- if (leaf) out.push({
17166
- path,
17167
- kind: leaf.kind,
17168
- ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
17169
- });
17186
+ if (leaf) {
17187
+ const quantity = quantityOfField(field);
17188
+ out.push({
17189
+ path,
17190
+ kind: leaf.kind,
17191
+ ...leaf.enumValues ? { enumValues: leaf.enumValues } : {},
17192
+ ...quantity !== void 0 ? { quantity } : {}
17193
+ });
17194
+ }
17170
17195
  }
17171
17196
  return out;
17172
17197
  }
@@ -17225,7 +17250,7 @@ function requiredTopLevelKeys(schema) {
17225
17250
  /**
17226
17251
  * The proper prefixes of `path` whose schema accepts `undefined`: a required
17227
17252
  * leaf under one of them is required only once something writes that parent
17228
- * (`remaining.unit` is required only when `remaining` is present).
17253
+ * (`remaining.kind` is required only when `remaining` is present).
17229
17254
  */
17230
17255
  function optionalAncestors(schema, path) {
17231
17256
  const segments = path.split(".");
@@ -17337,7 +17362,8 @@ var CompositionProblemCodeSchema = zod.z.enum([
17337
17362
  "item-field-reserved",
17338
17363
  "target-missing",
17339
17364
  "target-unreadable",
17340
- "not-replaceable"
17365
+ "not-replaceable",
17366
+ "native-capability-replace"
17341
17367
  ]);
17342
17368
  /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
17343
17369
  var CompositionProblemSchema = zod.z.object({
@@ -17403,9 +17429,11 @@ function compositionFieldKey(plan) {
17403
17429
  return `${plan.cap}.${path}`;
17404
17430
  }
17405
17431
  /**
17406
- * What a feature does to its target (D663): `new` builds a capability on a new
17407
- * device; on an existing device it `add`s a capability the device lacks, or
17408
- * `replace`s named fields of one it has natively.
17432
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
17433
+ * new device; on an existing device it `add`s a capability the device lacks.
17434
+ * Replacing a capability the device already serves is refused
17435
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
17436
+ * wire for a claim the hub already holds.
17409
17437
  */
17410
17438
  var CompositionFeatureModeSchema = zod.z.enum([
17411
17439
  "new",
@@ -18202,6 +18230,9 @@ function isNativeCap(target, cap) {
18202
18230
  const owner = target.nativeCaps.get(cap);
18203
18231
  return owner !== void 0 && owner !== "composer";
18204
18232
  }
18233
+ function isPlanProblem(value) {
18234
+ return "code" in value;
18235
+ }
18205
18236
  /** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
18206
18237
  function planTargetOf(composition, target) {
18207
18238
  if (composition.target.kind === "new") return {
@@ -18217,26 +18248,59 @@ function planTargetOf(composition, target) {
18217
18248
  found: target
18218
18249
  };
18219
18250
  }
18220
- function featureTargetOf(target, cap) {
18251
+ var NATIVE_REPLACE_KEPT = new Set([
18252
+ "not-composable",
18253
+ "unknown-capability",
18254
+ "reserved-variant"
18255
+ ]);
18256
+ /** A native override is refused, unless the cap cannot be composed at all — that reason stays. */
18257
+ function nativeReplaceOrBlock(refusal, probed) {
18258
+ const kept = probed.problems.filter((item) => NATIVE_REPLACE_KEPT.has(item.code));
18259
+ if (kept.length > 0) return {
18260
+ problems: kept,
18261
+ fields: [],
18262
+ features: [],
18263
+ events: []
18264
+ };
18265
+ return refused(refusal);
18266
+ }
18267
+ function featureTargetOf(target, cap, at) {
18221
18268
  if (target.kind === "new") return {
18222
18269
  mode: "new",
18223
18270
  deviceType: target.type
18224
18271
  };
18272
+ if (isNativeCap(target.found, cap)) return problem("native-capability-replace", at, `native-capability-replace: stored override replaces native capability \`${cap}\` on device ${target.found.deviceId} — refused (D944)`);
18225
18273
  return {
18226
- mode: isNativeCap(target.found, cap) ? "replace" : "add",
18274
+ mode: "add",
18227
18275
  deviceType: target.found.type
18228
18276
  };
18229
18277
  }
18230
18278
  /**
18231
18279
  * The static plan. `target` describes an EXISTING target device (one read);
18232
- * it is ignored for a new one. On an existing target each feature REPLACES a
18233
- * cap the device has natively, else ADDS it (D663).
18280
+ * it is ignored for a new one. On an existing target each feature ADDS a cap
18281
+ * the device lacks. A feature that would replace a native capability is
18282
+ * refused by name (D944). A new device is unchanged.
18234
18283
  */
18235
18284
  function planComposition(composition, lookupCap, target) {
18236
18285
  const planTarget = planTargetOf(composition, target);
18237
18286
  if ("code" in planTarget) return refused(planTarget);
18238
18287
  const results = composition.features.map((feature, index) => {
18239
- const result = feature.kind === "passthrough" ? refused(problem("reserved-variant", `features[${index}]`, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`)) : planFeature(feature, index, featureTargetOf(planTarget, feature.cap), lookupCap);
18288
+ const at = `features[${index}]`;
18289
+ if (feature.kind === "passthrough") return refused(problem("reserved-variant", at, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`));
18290
+ const featureTarget = featureTargetOf(planTarget, feature.cap, at);
18291
+ if (isPlanProblem(featureTarget)) {
18292
+ if (!compositionFeatureEnabled(feature) || planTarget.kind !== "existing") return {
18293
+ problems: [],
18294
+ fields: [],
18295
+ features: [],
18296
+ events: []
18297
+ };
18298
+ return nativeReplaceOrBlock(featureTarget, planFeature(feature, index, {
18299
+ mode: "add",
18300
+ deviceType: planTarget.found.type
18301
+ }, lookupCap));
18302
+ }
18303
+ const result = planFeature(feature, index, featureTarget, lookupCap);
18240
18304
  return compositionFeatureEnabled(feature) ? result : {
18241
18305
  problems: result.problems,
18242
18306
  fields: [],
@@ -18377,7 +18441,11 @@ function cycleProblems(composition, ctx, fields, self) {
18377
18441
  function validateComposition(composition, ctx) {
18378
18442
  const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
18379
18443
  const self = candidateSelf(composition, ctx, plan);
18380
- const cycles = cycleProblems(composition, ctx, plan.fields, self);
18444
+ const refusedFeatures = plan.problems.filter((item) => item.code === "native-capability-replace").map((item) => item.path);
18445
+ const cycles = cycleProblems(composition, ctx, plan.fields, self).filter((item) => {
18446
+ if (refusedFeatures.some((at) => item.path === at || item.path.startsWith(`${at}.`))) return false;
18447
+ return refusedFeatures.length === 0 || item.path !== "features";
18448
+ });
18381
18449
  const problems = [
18382
18450
  ...plan.problems,
18383
18451
  ...sourceProblems(plan.fields, ctx, self),
@@ -20894,12 +20962,12 @@ getAdminLink: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative()
20894
20962
  * providers detect renames by matching `uniqueId` and patch `id`
20895
20963
  * in-place so dispatch keeps working.
20896
20964
  *
20897
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20898
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20899
- * state push and are the single source of truth. The `device_class` tag is
20900
- * an HA-specific input that is consumed at the discovery boundary
20901
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20902
- * stored after that point.
20965
+ * **Display unit and precision are NOT stored here.** They are a read-time
20966
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20967
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20968
+ * neither is repeated on each sample. The `device_class` tag is an
20969
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20970
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20903
20971
  *
20904
20972
  * The `raw` escape hatch carries provider-specific attributes that don't
20905
20973
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20913,6 +20981,14 @@ var SourceInfoSchema = zod.z.object({
20913
20981
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20914
20982
  * Used to detect rename when `id` changes. */
20915
20983
  uniqueId: zod.z.string().optional(),
20984
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20985
+ quantity: ReadingQuantitySchema.optional(),
20986
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20987
+ dateTimeShape: zod.z.enum([
20988
+ "date",
20989
+ "time",
20990
+ "datetime"
20991
+ ]).optional(),
20916
20992
  /** Provider-specific extras that don't fit the structured slots.
20917
20993
  * Whitelist what goes here per provider — do NOT dump entire HA
20918
20994
  * attribute blobs (would explode the meta JSON column). */
@@ -20997,9 +21073,38 @@ function normalizeUnit(raw) {
20997
21073
  if (lower === "mph") return "mph";
20998
21074
  if (lower === "m/s") return "m/s";
20999
21075
  if (lower === "m") return "m";
21076
+ if (lower === "m²" || lower === "m2") return "m²";
21077
+ if (lower === "mm") return "mm";
21078
+ if (lower === "cm") return "cm";
21000
21079
  if (lower === "km") return "km";
21080
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
21081
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
21001
21082
  if (lower === "kg") return "kg";
21002
21083
  if (lower === "g") return "g";
21084
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
21085
+ if (lower === "ml") return "mL";
21086
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
21087
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
21088
+ if (lower === "ms") return "ms";
21089
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
21090
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
21091
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
21092
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
21093
+ if (lower === "kib") return "KiB";
21094
+ if (lower === "mib") return "MiB";
21095
+ if (lower === "gib") return "GiB";
21096
+ if (lower === "tib") return "TiB";
21097
+ if (lower === "kb") return "kB";
21098
+ if (lower === "mb") return "MB";
21099
+ if (lower === "gb") return "GB";
21100
+ if (lower === "dbm") return "dBm";
21101
+ if (lower === "dbfs") return "dBFS";
21102
+ if (lower === "inhg") return "inHg";
21103
+ if (lower === "mm/h") return "mm/h";
21104
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
21105
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
21106
+ if (lower === "kbps") return "kbps";
21107
+ if (lower === "fps") return "fps";
21003
21108
  return trimmed;
21004
21109
  }
21005
21110
  /**
@@ -42261,7 +42366,8 @@ var deviceManagerCapability = {
42261
42366
  "enum"
42262
42367
  ]),
42263
42368
  enumValues: zod.z.array(zod.z.string()).optional(),
42264
- item: zod.z.boolean().optional()
42369
+ item: zod.z.boolean().optional(),
42370
+ quantity: ReadingQuantitySchema.optional()
42265
42371
  })).readonly(),
42266
42372
  itemArray: zod.z.object({
42267
42373
  path: zod.z.string(),
@@ -47526,8 +47632,8 @@ var MaskPolygonShapeSchema = zod.z.object({
47526
47632
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
47527
47633
  var MaskGridShapeSchema = zod.z.object({
47528
47634
  kind: zod.z.literal("grid"),
47529
- gridWidth: zod.z.number(),
47530
- gridHeight: zod.z.number(),
47635
+ gridWidth: zod.z.number().meta(readingMeta("none")),
47636
+ gridHeight: zod.z.number().meta(readingMeta("none")),
47531
47637
  cells: zod.z.array(zod.z.boolean())
47532
47638
  });
47533
47639
  /** Directed line / tripwire — an ordered list of normalized points (≥2). */
@@ -62076,27 +62182,17 @@ var addonsCapability = {
62076
62182
  */
62077
62183
  var AirQualitySensorStatusSchema = zod.z.object({
62078
62184
  /** Carbon dioxide concentration in ppm. */
62079
- co2Ppm: zod.z.number().min(0).optional(),
62185
+ co2Ppm: zod.z.number().min(0).optional().meta(readingMeta("concentration-volume")),
62080
62186
  /** Total volatile organic compounds in ppb. */
62081
- vocPpb: zod.z.number().min(0).optional(),
62187
+ vocPpb: zod.z.number().min(0).optional().meta(readingMeta("concentration-ppb")),
62082
62188
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
62083
- pm25: zod.z.number().min(0).optional(),
62189
+ pm25: zod.z.number().min(0).optional().meta(readingMeta("concentration-mass")),
62084
62190
  /** Particulate matter ≤ 10 μm in µg/m³. */
62085
- pm10: zod.z.number().min(0).optional(),
62191
+ pm10: zod.z.number().min(0).optional().meta(readingMeta("concentration-mass")),
62086
62192
  /** Composite AQI value (typically 0..500). */
62087
- aqi: zod.z.number().optional(),
62193
+ aqi: zod.z.number().optional().meta(readingMeta("index")),
62088
62194
  /** Ms epoch when the slice was last updated. */
62089
- lastFetchedAt: zod.z.number(),
62090
- /** Live display unit of the single metric this slice carries (e.g. HA
62091
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
62092
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
62093
- * per slice is unambiguous. */
62094
- unit: zod.z.string().optional(),
62095
- /** Suggested decimal places for numeric display.
62096
- * Populated live from the upstream source when provided (e.g. HA
62097
- * `attributes.suggested_display_precision`). Falls back to
62098
- * auto-formatting when absent. */
62099
- precision: zod.z.number().int().min(0).max(10).optional()
62195
+ lastFetchedAt: zod.z.number()
62100
62196
  });
62101
62197
  var airQualitySensorCapability = {
62102
62198
  name: "air-quality-sensor",
@@ -62133,12 +62229,12 @@ var airQualitySensorCapability = {
62133
62229
  },
62134
62230
  pm25: {
62135
62231
  kind: "gauge",
62136
- unit: "ug/m3",
62232
+ unit: "µg/m³",
62137
62233
  deadband: 1
62138
62234
  },
62139
62235
  pm10: {
62140
62236
  kind: "gauge",
62141
- unit: "ug/m3",
62237
+ unit: "µg/m³",
62142
62238
  deadband: 1
62143
62239
  },
62144
62240
  aqi: {
@@ -62156,22 +62252,13 @@ var airQualitySensorCapability = {
62156
62252
  * entries with `device_class: illuminance`.
62157
62253
  */
62158
62254
  var AmbientLightSensorStatusSchema = zod.z.object({
62159
- /** Current illuminance in lux (lx). */
62160
- lux: zod.z.number().min(0).meta({
62161
- unit: "lx",
62255
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
62256
+ lux: zod.z.number().min(0).nullable().meta(readingMeta("illuminance", {
62257
+ precision: 0,
62162
62258
  label: "Illuminance"
62163
- }),
62259
+ })),
62164
62260
  /** Ms epoch when the slice was last updated. */
62165
- lastFetchedAt: zod.z.number(),
62166
- /** Live display unit from the upstream source (e.g. HA
62167
- * `attributes.unit_of_measurement`). The UI prefers this over the
62168
- * role's canonical unit. Absent → fall back to the canonical unit. */
62169
- unit: zod.z.string().optional(),
62170
- /** Suggested decimal places for numeric display.
62171
- * Populated live from the upstream source when provided (e.g. HA
62172
- * `attributes.suggested_display_precision`). Falls back to
62173
- * auto-formatting when absent. */
62174
- precision: zod.z.number().int().min(0).max(10).optional()
62261
+ lastFetchedAt: zod.z.number()
62175
62262
  });
62176
62263
  var ambientLightSensorCapability = {
62177
62264
  name: "ambient-light-sensor",
@@ -62203,12 +62290,7 @@ var ambientLightSensorCapability = {
62203
62290
  } } },
62204
62291
  /** Clock fields: written, but excluded from the compare that decides
62205
62292
  * whether persisting is worth a SQLite commit. */
62206
- volatileStateFields: ["lastFetchedAt"],
62207
- customize: {
62208
- version: 1,
62209
- advanced: "derive",
62210
- targets: []
62211
- }
62293
+ volatileStateFields: ["lastFetchedAt"]
62212
62294
  };
62213
62295
  /**
62214
62296
  * Per-class audio metrics aggregated over a sliding window.
@@ -62444,10 +62526,7 @@ var BatteryStatusSchema = zod.z.object({
62444
62526
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
62445
62527
  * declines to band a non-finite reading, which is the correct shape.
62446
62528
  */
62447
- percentage: zod.z.number().min(0).max(100).nullable().meta({
62448
- unit: "%",
62449
- label: "Charge"
62450
- }),
62529
+ percentage: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
62451
62530
  /**
62452
62531
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
62453
62532
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -62474,7 +62553,14 @@ var BatteryStatusSchema = zod.z.object({
62474
62553
  */
62475
62554
  sleeping: zod.z.boolean().meta({ label: "Sleeping" }),
62476
62555
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62477
- lastUpdated: zod.z.number(),
62556
+ lastUpdated: zod.z.number().meta(readingMeta("none")),
62557
+ /**
62558
+ * Battery voltage in volts, for a cell the firmware reports as voltage
62559
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
62560
+ * that only report charge. A second reading on this cap, not a second
62561
+ * `numeric-sensor` on the same device.
62562
+ */
62563
+ volts: zod.z.number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
62478
62564
  /**
62479
62565
  * Ms epoch of the last time the device PROVED it was reachable — a
62480
62566
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -62526,7 +62612,6 @@ var BATTERY_DEVICE_TYPES = [
62526
62612
  /** Generic Customize target for the percentage reading (D936). */
62527
62613
  var batteryCustomize = {
62528
62614
  version: 1,
62529
- advanced: "derive",
62530
62615
  targets: [{
62531
62616
  id: "percentage",
62532
62617
  cardinality: "scalar",
@@ -62535,7 +62620,7 @@ var batteryCustomize = {
62535
62620
  stateField: "percentage"
62536
62621
  },
62537
62622
  deviceTypes: BATTERY_DEVICE_TYPES,
62538
- apply: "add-or-replace",
62623
+ apply: "add",
62539
62624
  path: "percentage",
62540
62625
  sources: [{
62541
62626
  kind: "field",
@@ -62656,10 +62741,16 @@ onStatusChanged: { data: zod.z.object({
62656
62741
  /**
62657
62742
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
62658
62743
  */
62659
- history: { fields: { percentage: {
62660
- kind: "gauge",
62661
- unit: "%"
62662
- } } },
62744
+ history: { fields: {
62745
+ percentage: {
62746
+ kind: "gauge",
62747
+ unit: "%"
62748
+ },
62749
+ volts: {
62750
+ kind: "gauge",
62751
+ unit: "V"
62752
+ }
62753
+ } },
62663
62754
  customize: batteryCustomize,
62664
62755
  /** Clock fields: written, but excluded from the compare that decides
62665
62756
  * whether persisting is worth a SQLite commit. */
@@ -62706,12 +62797,7 @@ var binaryCapability = {
62706
62797
  history: { fields: { on: { kind: "boolean" } } },
62707
62798
  /** Clock fields: written, but excluded from the compare that decides
62708
62799
  * whether persisting is worth a SQLite commit. */
62709
- volatileStateFields: ["lastChangedAt"],
62710
- customize: {
62711
- version: 1,
62712
- advanced: "derive",
62713
- targets: []
62714
- }
62800
+ volatileStateFields: ["lastChangedAt"]
62715
62801
  };
62716
62802
  /**
62717
62803
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -62727,7 +62813,7 @@ var binaryCapability = {
62727
62813
  */
62728
62814
  var BrightnessStatusSchema = zod.z.object({
62729
62815
  /** Current level as 0..100 inclusive. Firmware-reported. */
62730
- percentage: zod.z.number().min(0).max(100),
62816
+ percentage: zod.z.number().min(0).max(100).meta(readingMeta("percentage")),
62731
62817
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
62732
62818
  lastChangedAt: zod.z.number()
62733
62819
  });
@@ -63278,21 +63364,21 @@ var ClimateControlStatusSchema = zod.z.object({
63278
63364
  /** Single setpoint in Celsius. Used by single-target modes
63279
63365
  * (heat / cool / dry). Null when the device is in a range mode
63280
63366
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
63281
- target: zod.z.number().nullable(),
63367
+ target: zod.z.number().nullable().meta(readingMeta("temperature")),
63282
63368
  /** Upper bound of the dual setpoint in Celsius. Populated only
63283
63369
  * in `heat_cool` mode. */
63284
- targetHigh: zod.z.number().nullable(),
63370
+ targetHigh: zod.z.number().nullable().meta(readingMeta("temperature")),
63285
63371
  /** Lower bound of the dual setpoint in Celsius. Populated only
63286
63372
  * in `heat_cool` mode. */
63287
- targetLow: zod.z.number().nullable(),
63373
+ targetLow: zod.z.number().nullable().meta(readingMeta("temperature")),
63288
63374
  /** Target relative humidity (0..100). Null when the device has no
63289
63375
  * humidity control. */
63290
- targetHumidity: zod.z.number().min(0).max(100).nullable(),
63376
+ targetHumidity: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity")),
63291
63377
  /** Current measured temperature in Celsius. Read-only — pushed by
63292
63378
  * the device's internal sensor. */
63293
- currentTemp: zod.z.number().nullable(),
63379
+ currentTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
63294
63380
  /** Current measured relative humidity (0..100). Read-only. */
63295
- currentHumidity: zod.z.number().min(0).max(100).nullable(),
63381
+ currentHumidity: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity")),
63296
63382
  /** Vertical louver swing on/off. Null when the device has no vertical
63297
63383
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
63298
63384
  swingVertical: zod.z.boolean().nullable(),
@@ -63300,11 +63386,11 @@ var ClimateControlStatusSchema = zod.z.object({
63300
63386
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
63301
63387
  swingHorizontal: zod.z.boolean().nullable(),
63302
63388
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
63303
- minTemp: zod.z.number().nullable().optional(),
63389
+ minTemp: zod.z.number().nullable().optional().meta(readingMeta("temperature")),
63304
63390
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
63305
- maxTemp: zod.z.number().nullable().optional(),
63391
+ maxTemp: zod.z.number().nullable().optional().meta(readingMeta("temperature")),
63306
63392
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
63307
- tempStep: zod.z.number().positive().nullable().optional(),
63393
+ tempStep: zod.z.number().positive().nullable().optional().meta(readingMeta("temperature")),
63308
63394
  /** Ms epoch when the slice was last updated (push or command). */
63309
63395
  lastFetchedAt: zod.z.number()
63310
63396
  });
@@ -63446,17 +63532,17 @@ var climateControlCapability = {
63446
63532
  * modes to expose in the picker without a slice fetch.
63447
63533
  */
63448
63534
  var RgbTripletSchema = zod.z.object({
63449
- r: zod.z.number().int().min(0).max(255),
63450
- g: zod.z.number().int().min(0).max(255),
63451
- b: zod.z.number().int().min(0).max(255)
63535
+ r: zod.z.number().int().min(0).max(255).meta(readingMeta("none")),
63536
+ g: zod.z.number().int().min(0).max(255).meta(readingMeta("none")),
63537
+ b: zod.z.number().int().min(0).max(255).meta(readingMeta("none"))
63452
63538
  });
63453
63539
  var HsvTripletSchema = zod.z.object({
63454
63540
  /** Hue in degrees, 0..360. */
63455
- h: zod.z.number().min(0).max(360),
63541
+ h: zod.z.number().min(0).max(360).meta(readingMeta("none")),
63456
63542
  /** Saturation as 0..100 inclusive. */
63457
- s: zod.z.number().min(0).max(100),
63543
+ s: zod.z.number().min(0).max(100).meta(readingMeta("none")),
63458
63544
  /** Value/brightness as 0..100 inclusive. */
63459
- v: zod.z.number().min(0).max(100)
63545
+ v: zod.z.number().min(0).max(100).meta(readingMeta("none"))
63460
63546
  });
63461
63547
  var ColorInputSchema = zod.z.discriminatedUnion("mode", [
63462
63548
  zod.z.object({
@@ -63485,7 +63571,7 @@ var ColorStatusSchema = zod.z.object({
63485
63571
  /** Populated when `mode === 'hsv'`. */
63486
63572
  hsv: HsvTripletSchema.optional(),
63487
63573
  /** Populated when `mode === 'mired'`. */
63488
- mireds: zod.z.number().int().optional(),
63574
+ mireds: zod.z.number().int().optional().meta(readingMeta("colour-temperature")),
63489
63575
  /** Ms epoch of the last operator-driven change. */
63490
63576
  lastChangedAt: zod.z.number()
63491
63577
  });
@@ -63688,18 +63774,12 @@ zod.z.enum([
63688
63774
  "empty",
63689
63775
  "full"
63690
63776
  ]);
63691
- /** Units a remaining-life counter is reported in. Closed: the panel renders
63692
- * each. */
63693
- var ConsumableRemainingUnitSchema = zod.z.enum([
63694
- "days",
63695
- "hours",
63696
- "cycles"
63697
- ]);
63698
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
63699
- * days). `value` null = unknown (D393). */
63777
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
63778
+ var ConsumableRemainingKindSchema = zod.z.enum(["duration", "count"]);
63779
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
63700
63780
  var ConsumableRemainingSchema = zod.z.object({
63701
- value: zod.z.number().min(0).nullable(),
63702
- unit: ConsumableRemainingUnitSchema
63781
+ value: zod.z.number().min(0).nullable().meta(readingMeta("device")),
63782
+ kind: ConsumableRemainingKindSchema
63703
63783
  });
63704
63784
  /** A single consumable item. Either a continuous `level` (remaining
63705
63785
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -63712,7 +63792,7 @@ var ConsumableItemSchema = zod.z.object({
63712
63792
  /** Display name. */
63713
63793
  label: zod.z.string().min(1),
63714
63794
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
63715
- level: zod.z.number().min(0).max(100).nullable(),
63795
+ level: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63716
63796
  /** The provider's discrete verdict when known (binary mode). */
63717
63797
  status: ConsumableStatusSchema.nullable(),
63718
63798
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -63794,8 +63874,8 @@ var consumablesCustomize = {
63794
63874
  transforms: [{ kind: "identity" }]
63795
63875
  }],
63796
63876
  literals: [{
63797
- path: "remaining.unit",
63798
- values: [...ConsumableRemainingUnitSchema.options]
63877
+ path: "remaining.kind",
63878
+ values: [...ConsumableRemainingKindSchema.options]
63799
63879
  }]
63800
63880
  },
63801
63881
  {
@@ -63924,7 +64004,6 @@ reset: method(zod.z.object({
63924
64004
  },
63925
64005
  "remaining.value": {
63926
64006
  kind: "gauge",
63927
- unitFrom: "remaining.unit",
63928
64007
  deadband: 1,
63929
64008
  displayName: "Remaining"
63930
64009
  }
@@ -64023,49 +64102,26 @@ var ControlKindSchema = zod.z.enum([
64023
64102
  * (empty array for the other kinds). It lives in the slice because
64024
64103
  * HA `select.*` entities may update their options at runtime.
64025
64104
  *
64026
- * The `unit`/`min`/`max`/`step` fields are meaningful for
64027
- * `kind === 'numeric'` and are absent (undefined) for all other
64028
- * kinds. They are populated live from HA attributes on each state
64029
- * push — consistent with `options` and the sensor-unit approach.
64105
+ * Numeric `min` / `max` / `step` live in the slice and share the device
64106
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
64107
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
64030
64108
  */
64031
64109
  var ControlStatusSchema = zod.z.object({
64032
64110
  kind: ControlKindSchema,
64033
64111
  /** Current value. Numeric kind → number; select/text/datetime → string.
64034
64112
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
64035
64113
  * or time-only — HA accepts all three forms). */
64036
- value: zod.z.union([zod.z.number(), zod.z.string()]),
64114
+ value: zod.z.union([zod.z.number().meta(readingMeta("device")), zod.z.string()]),
64037
64115
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
64038
64116
  options: zod.z.array(zod.z.string()),
64039
64117
  /** Ms epoch when the slice was last updated. */
64040
64118
  lastChangedAt: zod.z.number(),
64041
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
64042
- * Populated live from HA `attributes.unit_of_measurement` on each push.
64043
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
64044
- unit: zod.z.string().optional(),
64045
- /** Minimum allowed value. Populated live from HA `attributes.min`.
64046
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
64047
- min: zod.z.number().optional(),
64048
- /** Maximum allowed value. Populated live from HA `attributes.max`.
64049
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
64050
- max: zod.z.number().optional(),
64051
- /** Step increment. Populated live from HA `attributes.step`.
64052
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
64053
- step: zod.z.number().optional(),
64054
- /** Suggested decimal places for numeric display. Populated live from HA
64055
- * `attributes.suggested_display_precision`, or derived from `step` when the
64056
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
64057
- * other kinds. Falls back to auto-formatting when absent. */
64058
- precision: zod.z.number().int().min(0).max(10).optional(),
64059
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
64060
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
64061
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
64062
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
64063
- * `has_date`/`has_time` attributes on each push. */
64064
- format: zod.z.enum([
64065
- "date",
64066
- "time",
64067
- "datetime"
64068
- ]).optional()
64119
+ /** Minimum allowed value, in the device quantity's base unit. */
64120
+ min: zod.z.number().optional().meta(readingMeta("device")),
64121
+ /** Maximum allowed value, in the device quantity's base unit. */
64122
+ max: zod.z.number().optional().meta(readingMeta("device")),
64123
+ /** Step increment, in the device quantity's base unit. */
64124
+ step: zod.z.number().optional().meta(readingMeta("device"))
64069
64125
  });
64070
64126
  var ControlSetValueInputSchema = zod.z.discriminatedUnion("kind", [
64071
64127
  zod.z.object({
@@ -64153,9 +64209,9 @@ var CoverStatusSchema = zod.z.object({
64153
64209
  state: CoverStateSchema,
64154
64210
  /** 0 = fully closed, 100 = fully open. Null when the device has
64155
64211
  * no intermediate position surface. */
64156
- position: zod.z.number().min(0).max(100).nullable(),
64212
+ position: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64157
64213
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
64158
- tiltPosition: zod.z.number().min(0).max(100).nullable(),
64214
+ tiltPosition: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64159
64215
  /** Ms epoch when the slice was last updated. */
64160
64216
  lastChangedAt: zod.z.number()
64161
64217
  });
@@ -64258,9 +64314,9 @@ var NormalizedRangeSchema$1 = zod.z.object({
64258
64314
  var DayNightStatusSchema = zod.z.object({
64259
64315
  mode: DayNightModeSchema,
64260
64316
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
64261
- sensitivity: zod.z.number().optional(),
64317
+ sensitivity: zod.z.number().optional().meta(readingMeta("none")),
64262
64318
  /** Delay before the IR-cut filter flips, in seconds. */
64263
- switchDelaySec: zod.z.number().optional(),
64319
+ switchDelaySec: zod.z.number().optional().meta(readingMeta("duration")),
64264
64320
  lastFetchedAt: zod.z.number()
64265
64321
  });
64266
64322
  /**
@@ -64487,11 +64543,11 @@ var DishwasherStatusSchema = zod.z.object({
64487
64543
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
64488
64544
  availablePrograms: zod.z.array(zod.z.string()),
64489
64545
  /** Seconds remaining on the active cycle, or null when not running / not known. */
64490
- remainingSeconds: zod.z.number().int().nonnegative().nullable(),
64546
+ remainingSeconds: zod.z.number().int().nonnegative().nullable().meta(readingMeta("duration")),
64491
64547
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
64492
- delayedStartSeconds: zod.z.number().int().nonnegative().nullable(),
64548
+ delayedStartSeconds: zod.z.number().int().nonnegative().nullable().meta(readingMeta("duration")),
64493
64549
  /** Cycle completion 0..100, or null when not applicable. */
64494
- progressPercent: zod.z.number().min(0).max(100).nullable(),
64550
+ progressPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64495
64551
  /** True when the door is reported open; null when the door state is not known. */
64496
64552
  doorOpen: zod.z.boolean().nullable(),
64497
64553
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -64515,7 +64571,7 @@ var DishwasherStatusSchema = zod.z.object({
64515
64571
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
64516
64572
  machineCareDue: zod.z.boolean().nullable(),
64517
64573
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
64518
- machineCareRemainingRuns: zod.z.number().int().nonnegative().nullable(),
64574
+ machineCareRemainingRuns: zod.z.number().int().nonnegative().nullable().meta(readingMeta("count")),
64519
64575
  /** Option states: null when the appliance does not expose the option or has not said. */
64520
64576
  halfLoad: zod.z.boolean().nullable(),
64521
64577
  hygienePlus: zod.z.boolean().nullable(),
@@ -64530,7 +64586,7 @@ var DishwasherStatusSchema = zod.z.object({
64530
64586
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
64531
64587
  activeEvents: zod.z.array(zod.z.string()),
64532
64588
  /** Numeric error code when the appliance reports one; null otherwise. */
64533
- errorCode: zod.z.number().nullable(),
64589
+ errorCode: zod.z.number().nullable().meta(readingMeta("none")),
64534
64590
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
64535
64591
  errorLabel: zod.z.string().nullable(),
64536
64592
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -64761,7 +64817,7 @@ var DoorbellStatusSchema = zod.z.object({
64761
64817
  /** Ms epoch of the last press. null = never observed since this provider started. */
64762
64818
  lastPressedAt: zod.z.number().nullable(),
64763
64819
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
64764
- pressCountSinceStart: zod.z.number()
64820
+ pressCountSinceStart: zod.z.number().meta(readingMeta("count"))
64765
64821
  });
64766
64822
  var DoorbellPressEventSchema = zod.z.object({
64767
64823
  deviceId: zod.z.number(),
@@ -64826,17 +64882,7 @@ onPressed: {
64826
64882
  history: "none",
64827
64883
  customize: doorbellCustomize
64828
64884
  };
64829
- /**
64830
- * Enum-state sensor — a string value picked from a finite option set.
64831
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
64832
- * states, weather conditions, contact-source channels, mode strings).
64833
- * The option set is stable across observations; it is persisted in the
64834
- * device config blob (via the control slice at adoption) and is NOT
64835
- * stored in `sourceInfo`.
64836
- */
64837
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
64838
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
64839
- var EnumSensorDateTimeFormatSchema = zod.z.enum([
64885
+ zod.z.enum([
64840
64886
  "date",
64841
64887
  "time",
64842
64888
  "datetime"
@@ -64844,12 +64890,11 @@ var EnumSensorDateTimeFormatSchema = zod.z.enum([
64844
64890
  var EnumSensorStatusSchema = zod.z.object({
64845
64891
  value: zod.z.string(),
64846
64892
  /**
64847
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
64848
- * as a locale date/time/datetime; absent for plain enum sensors. HA
64849
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
64850
- * a time-only sensor → `'time'`.
64893
+ * Set when `value` is a timestamp rather than an enum state, so history
64894
+ * does not learn ISO strings as states. The display shape (date / time /
64895
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
64851
64896
  */
64852
- format: EnumSensorDateTimeFormatSchema.optional(),
64897
+ timestamp: zod.z.boolean().optional(),
64853
64898
  /** Ms epoch when the slice was last updated. */
64854
64899
  lastFetchedAt: zod.z.number()
64855
64900
  });
@@ -64876,11 +64921,11 @@ var enumSensorCapability = {
64876
64921
  * History (D850): the value as an `enum` series (an appliance's program or
64877
64922
  * operation state, an HVAC action). The option set is per device, so the
64878
64923
  * value list is LEARNED as values are first seen (bounded). A sensor with a
64879
- * `format` carries a timestamp, not a state, and is not recorded.
64924
+ * `timestamp` carries a clock, not a state, and is not recorded.
64880
64925
  */
64881
64926
  history: { fields: { value: {
64882
64927
  kind: "enum",
64883
- skipWhenSet: "format"
64928
+ skipWhenSet: "timestamp"
64884
64929
  } } },
64885
64930
  /** Clock fields: written, but excluded from the compare that decides
64886
64931
  * whether persisting is worth a SQLite commit. */
@@ -64900,13 +64945,13 @@ var EventFireSchema = zod.z.object({
64900
64945
  deviceId: zod.z.number(),
64901
64946
  eventType: zod.z.string(),
64902
64947
  data: zod.z.record(zod.z.string(), zod.z.unknown()).nullable(),
64903
- timestamp: zod.z.number(),
64904
- seq: zod.z.number()
64948
+ timestamp: zod.z.number().meta(readingMeta("none")),
64949
+ seq: zod.z.number().meta(readingMeta("count"))
64905
64950
  });
64906
64951
  var EventEmitterStatusSchema = zod.z.object({
64907
64952
  eventTypes: zod.z.array(zod.z.string()),
64908
64953
  lastEvent: EventFireSchema.nullable(),
64909
- eventCountSinceStart: zod.z.number()
64954
+ eventCountSinceStart: zod.z.number().meta(readingMeta("count"))
64910
64955
  });
64911
64956
  var eventEmitterCapability = {
64912
64957
  name: "event-emitter",
@@ -65359,10 +65404,10 @@ var FanDirectionSchema = zod.z.enum(["forward", "reverse"]);
65359
65404
  var FanControlStatusSchema = zod.z.object({
65360
65405
  /** Active speed as 0..100 inclusive. Null when the device has no
65361
65406
  * speed surface (single-speed fan). */
65362
- percentage: zod.z.number().min(0).max(100).nullable(),
65407
+ percentage: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65363
65408
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
65364
65409
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
65365
- percentageStep: zod.z.number().positive().optional(),
65410
+ percentageStep: zod.z.number().positive().optional().meta(readingMeta("percentage")),
65366
65411
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
65367
65412
  * — empty string when no preset is active or supported. */
65368
65413
  preset: zod.z.string(),
@@ -65483,7 +65528,7 @@ var FeatureProbeStatusSchema = zod.z.object({
65483
65528
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
65484
65529
  model: zod.z.string().nullable(),
65485
65530
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
65486
- channelCount: zod.z.number().nullable(),
65531
+ channelCount: zod.z.number().nullable().meta(readingMeta("count")),
65487
65532
  /**
65488
65533
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
65489
65534
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -65608,7 +65653,7 @@ var gasCapability = {
65608
65653
  };
65609
65654
  var HeatingValveStatusSchema = zod.z.object({
65610
65655
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
65611
- position: zod.z.number().min(0).max(100).nullable(),
65656
+ position: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65612
65657
  /** Ms epoch when the slice was last updated (push or poll). */
65613
65658
  lastFetchedAt: zod.z.number()
65614
65659
  });
@@ -65651,9 +65696,9 @@ var HumidifierStatusSchema = zod.z.object({
65651
65696
  /** Whether the humidifier is currently on. */
65652
65697
  on: zod.z.boolean(),
65653
65698
  /** Current measured relative humidity (0..100). Null when not reported. */
65654
- currentHumidity: zod.z.number().min(0).max(100).nullable(),
65699
+ currentHumidity: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65655
65700
  /** Target relative humidity (0..100). Null when no setpoint surface. */
65656
- targetHumidity: zod.z.number().min(0).max(100).nullable(),
65701
+ targetHumidity: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65657
65702
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
65658
65703
  * device has no mode surface. */
65659
65704
  mode: zod.z.string().nullable(),
@@ -65663,9 +65708,9 @@ var HumidifierStatusSchema = zod.z.object({
65663
65708
  * `idle` / `off`). Null when the device doesn't report it. */
65664
65709
  action: zod.z.string().nullable(),
65665
65710
  /** HA `min_humidity` attribute. Null → UI uses 0. */
65666
- minHumidity: zod.z.number().nullable(),
65711
+ minHumidity: zod.z.number().nullable().meta(readingMeta("humidity")),
65667
65712
  /** HA `max_humidity` attribute. Null → UI uses 100. */
65668
- maxHumidity: zod.z.number().nullable(),
65713
+ maxHumidity: zod.z.number().nullable().meta(readingMeta("humidity")),
65669
65714
  /** Ms epoch when the slice was last updated. */
65670
65715
  lastChangedAt: zod.z.number()
65671
65716
  });
@@ -65735,22 +65780,13 @@ var humidifierCapability = {
65735
65780
  * `sourceInfo`.
65736
65781
  */
65737
65782
  var HumiditySensorStatusSchema = zod.z.object({
65738
- /** Current relative humidity, 0..100. */
65739
- percent: zod.z.number().min(0).max(100).meta({
65740
- unit: "%",
65783
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
65784
+ percent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity", {
65785
+ precision: 0,
65741
65786
  label: "Humidity"
65742
- }),
65787
+ })),
65743
65788
  /** Ms epoch when the slice was last updated. */
65744
- lastFetchedAt: zod.z.number(),
65745
- /** Live display unit from the upstream source (e.g. HA
65746
- * `attributes.unit_of_measurement`). The UI prefers this over the
65747
- * role's canonical unit. Absent → fall back to the canonical unit. */
65748
- unit: zod.z.string().optional(),
65749
- /** Suggested decimal places for numeric display.
65750
- * Populated live from the upstream source when provided (e.g. HA
65751
- * `attributes.suggested_display_precision`). Falls back to
65752
- * auto-formatting when absent. */
65753
- precision: zod.z.number().int().min(0).max(10).optional()
65789
+ lastFetchedAt: zod.z.number()
65754
65790
  });
65755
65791
  var humiditySensorCapability = {
65756
65792
  name: "humidity-sensor",
@@ -65781,12 +65817,7 @@ var humiditySensorCapability = {
65781
65817
  } } },
65782
65818
  /** Clock fields: written, but excluded from the compare that decides
65783
65819
  * whether persisting is worth a SQLite commit. */
65784
- volatileStateFields: ["lastFetchedAt"],
65785
- customize: {
65786
- version: 1,
65787
- advanced: "derive",
65788
- targets: []
65789
- }
65820
+ volatileStateFields: ["lastFetchedAt"]
65790
65821
  };
65791
65822
  /**
65792
65823
  * Image display cap. Models a single still image exposed by an integration —
@@ -65807,7 +65838,7 @@ var ImageStatusSchema = zod.z.object({
65807
65838
  * entity exposes no `entity_picture` (yet). */
65808
65839
  url: zod.z.string().nullable(),
65809
65840
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
65810
- lastUpdated: zod.z.number().nullable()
65841
+ lastUpdated: zod.z.number().nullable().meta(readingMeta("none"))
65811
65842
  });
65812
65843
  var imageCapability = {
65813
65844
  name: "image",
@@ -65895,19 +65926,19 @@ var NormalizedRangeSchema = zod.z.object({
65895
65926
  */
65896
65927
  var ImageSettingsStatusSchema = zod.z.object({
65897
65928
  /** Normalized 0–100. */
65898
- brightness: zod.z.number().optional(),
65929
+ brightness: zod.z.number().optional().meta(readingMeta("none")),
65899
65930
  /** Normalized 0–100. */
65900
- contrast: zod.z.number().optional(),
65931
+ contrast: zod.z.number().optional().meta(readingMeta("none")),
65901
65932
  /** Normalized 0–100. */
65902
- saturation: zod.z.number().optional(),
65933
+ saturation: zod.z.number().optional().meta(readingMeta("none")),
65903
65934
  /** Normalized 0–100. */
65904
- sharpness: zod.z.number().optional(),
65935
+ sharpness: zod.z.number().optional().meta(readingMeta("none")),
65905
65936
  mirror: zod.z.boolean().optional(),
65906
65937
  flip: zod.z.boolean().optional(),
65907
65938
  rotate: ImageRotateSchema.optional(),
65908
65939
  whiteBalance: WhiteBalanceModeSchema.optional(),
65909
65940
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
65910
- warmth: zod.z.number().optional(),
65941
+ warmth: zod.z.number().optional().meta(readingMeta("none")),
65911
65942
  exposureMode: ExposureModeSchema.optional(),
65912
65943
  backlightMode: BacklightModeSchema.optional(),
65913
65944
  lastFetchedAt: zod.z.number()
@@ -66204,10 +66235,10 @@ var IntercomAbilitySchema = zod.z.object({
66204
66235
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
66205
66236
  codecs: zod.z.array(zod.z.string()),
66206
66237
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
66207
- sampleRate: zod.z.number(),
66238
+ sampleRate: zod.z.number().meta(readingMeta("none")),
66208
66239
  duplex: zod.z.enum(["half", "full"]),
66209
66240
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
66210
- maxBacklogMs: zod.z.number()
66241
+ maxBacklogMs: zod.z.number().meta(readingMeta("duration-ms"))
66211
66242
  });
66212
66243
  var IntercomStatusSchema = zod.z.object({
66213
66244
  /** True while a talk session is open. */
@@ -66434,13 +66465,13 @@ var LawnMowerControlStatusSchema = zod.z.object({
66434
66465
  activity: LawnMowerActivitySchema,
66435
66466
  /** 0..100 battery percentage. Null when the device has no battery
66436
66467
  * reading. */
66437
- batteryLevel: zod.z.number().min(0).max(100).nullable(),
66468
+ batteryLevel: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66438
66469
  /** 0..100 mowing-completion percentage of the current task, or null when no
66439
66470
  * task is active / progress is unavailable. */
66440
- progressPercent: zod.z.number().min(0).max(100).nullable(),
66471
+ progressPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66441
66472
  /** Current device/event code (dynamic — mostly status, sometimes an error),
66442
66473
  * or null when unknown. */
66443
- currentCode: zod.z.number().nullable(),
66474
+ currentCode: zod.z.number().nullable().meta(readingMeta("none")),
66444
66475
  /** Human label for {@link currentCode}, or null when undecodable. */
66445
66476
  currentCodeLabel: zod.z.string().nullable(),
66446
66477
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -67164,7 +67195,7 @@ var MediaPlayerStatusSchema = zod.z.object({
67164
67195
  state: MediaPlayerStateSchema,
67165
67196
  /** Volume as 0..100 inclusive. Null when the device has no volume
67166
67197
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
67167
- volumeLevel: zod.z.number().min(0).max(100).nullable(),
67198
+ volumeLevel: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67168
67199
  /** Mute toggle distinct from volume=0. Null when no mute surface.
67169
67200
  * Pair with `DeviceFeature.MediaPlayerMute`. */
67170
67201
  isMuted: zod.z.boolean().nullable(),
@@ -67177,10 +67208,10 @@ var MediaPlayerStatusSchema = zod.z.object({
67177
67208
  currentMedia: MediaInfoSchema.nullable(),
67178
67209
  /** Current playback position in ms. Null when not seekable or
67179
67210
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
67180
- positionMs: zod.z.number().int().nonnegative().nullable(),
67211
+ positionMs: zod.z.number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
67181
67212
  /** Total duration of the current media in ms. Null when unknown
67182
67213
  * (live stream) or nothing is playing. */
67183
- durationMs: zod.z.number().int().nonnegative().nullable(),
67214
+ durationMs: zod.z.number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
67184
67215
  /** Shuffle toggle. Null when no shuffle surface.
67185
67216
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
67186
67217
  shuffle: zod.z.boolean().nullable(),
@@ -67550,7 +67581,7 @@ var motionCustomize = {
67550
67581
  eventName: "onMotionChanged"
67551
67582
  },
67552
67583
  deviceTypes: [DeviceType.Camera],
67553
- apply: "replace",
67584
+ apply: "add",
67554
67585
  applyAuthority: {
67555
67586
  kind: "camera-sensors",
67556
67587
  timeoutField: "motionCooldownMs"
@@ -67647,7 +67678,7 @@ var MotionStatusSchema = zod.z.object({
67647
67678
  * it, every provider seeded it at boot, and every session left it
67648
67679
  * `detected: true` after its detach.
67649
67680
  */
67650
- autoClearAfterMs: zod.z.number().nullable()
67681
+ autoClearAfterMs: zod.z.number().nullable().meta(readingMeta("duration-ms"))
67651
67682
  });
67652
67683
  /**
67653
67684
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -67829,7 +67860,7 @@ var MotionZoneRegionSchema = zod.z.object({
67829
67860
  * the grid region(s). */
67830
67861
  var MotionZoneStatusSchema = zod.z.object({
67831
67862
  enabled: zod.z.boolean(),
67832
- sensitivity: zod.z.number(),
67863
+ sensitivity: zod.z.number().meta(readingMeta("none")),
67833
67864
  /** Grid region(s). Today exactly one `grid` shape. */
67834
67865
  regions: zod.z.array(MotionZoneRegionSchema),
67835
67866
  lastFetchedAt: zod.z.number()
@@ -68201,7 +68232,7 @@ var NavigationStatusSchema = zod.z.object({
68201
68232
  following: zod.z.boolean(),
68202
68233
  flash: zod.z.boolean(),
68203
68234
  lightMode: NavigationLightModeSchema,
68204
- lightLevel: zod.z.number().min(40).max(100),
68235
+ lightLevel: zod.z.number().min(40).max(100).meta(readingMeta("none")),
68205
68236
  /** Ms epoch when the slice was last updated. */
68206
68237
  lastChangedAt: zod.z.number()
68207
68238
  });
@@ -68335,13 +68366,13 @@ var NetworkLinkStatusSchema = zod.z.object({
68335
68366
  * one whose reading has not landed must not be drawn at 0 %. Consumers
68336
68367
  * SKIP a null rather than coerce it.
68337
68368
  */
68338
- signalPercent: zod.z.number().min(0).max(100).nullable(),
68369
+ signalPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68339
68370
  /** Raw received signal strength in dBm, when the firmware reports one. */
68340
- rssiDbm: zod.z.number().optional(),
68371
+ rssiDbm: zod.z.number().optional().meta(readingMeta("signal-level")),
68341
68372
  /** Network name of a wireless link, when the firmware reports it. */
68342
68373
  ssid: zod.z.string().optional(),
68343
68374
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
68344
- lastUpdated: zod.z.number()
68375
+ lastUpdated: zod.z.number().meta(readingMeta("none"))
68345
68376
  });
68346
68377
  var networkLinkCapability = {
68347
68378
  name: "network-link",
@@ -68791,7 +68822,7 @@ var NotifierStatusSchema = zod.z.object({
68791
68822
  lastError: zod.z.string().nullable(),
68792
68823
  /** Number of deliveries currently buffered server-side (rate-limited
68793
68824
  * notifiers may queue). 0 when the provider sends synchronously. */
68794
- queueDepth: zod.z.number().int().nonnegative(),
68825
+ queueDepth: zod.z.number().int().nonnegative().meta(readingMeta("count")),
68795
68826
  /** Per-feature capability matrix — authoritative source for the
68796
68827
  * compose-form field gating. */
68797
68828
  supports: NotifierSupportsSchema
@@ -68883,11 +68914,9 @@ onSent: { data: zod.z.object({
68883
68914
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
68884
68915
  * for: water flow, distance, weight, frequency, signal strength, …).
68885
68916
  *
68886
- * The `unit` and `precision` fields carry the live display metadata that
68887
- * the upstream source provides with each state push (HA
68888
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
68889
- * The UI reads them directly from the slice — they are the single source
68890
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
68917
+ * The quantity is not on this slice. It is chosen when the device is
68918
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
68919
+ * that quantity's base unit. `null` is a refused sample (D393).
68891
68920
  *
68892
68921
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
68893
68922
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -68896,15 +68925,8 @@ onSent: { data: zod.z.object({
68896
68925
  * numeric values land here.
68897
68926
  */
68898
68927
  var NumericSensorStatusSchema = zod.z.object({
68899
- value: zod.z.number(),
68900
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
68901
- * Populated live from the upstream source on each state push.
68902
- * Absent for unitless sensors. */
68903
- unit: zod.z.string().optional(),
68904
- /** Suggested decimal places for numeric display.
68905
- * Populated live from the upstream source when provided.
68906
- * Falls back to auto-formatting when absent. */
68907
- precision: zod.z.number().int().min(0).max(10).optional(),
68928
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
68929
+ value: zod.z.number().nullable().meta(readingMeta("device")),
68908
68930
  /** Ms epoch when the slice was last updated. */
68909
68931
  lastFetchedAt: zod.z.number()
68910
68932
  });
@@ -68928,11 +68950,11 @@ var numericSensorCapability = {
68928
68950
  */
68929
68951
  durability: "restored",
68930
68952
  /**
68931
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
68953
+ * History (D850): recorded series. The unit is the device quantity's base,
68954
+ * resolved when the series is read — the cap has no single unit.
68932
68955
  */
68933
68956
  history: { fields: { value: {
68934
68957
  kind: "gauge",
68935
- unitFrom: "unit",
68936
68958
  minIntervalMs: 3e4
68937
68959
  } } },
68938
68960
  /** Clock fields: written, but excluded from the compare that decides
@@ -69333,23 +69355,26 @@ var PetFeederStatusSchema = zod.z.object({
69333
69355
  /** Food currently in the bowl (grams). Null when the device has not
69334
69356
  * reported a reading yet. On dual-hopper models this is the combined
69335
69357
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
69336
- foodLevel: zod.z.number().nullable(),
69358
+ foodLevel: zod.z.number().nullable().meta(readingMeta("none")),
69337
69359
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
69338
69360
  * single-hopper models. */
69339
- food1: zod.z.number().nullable(),
69361
+ food1: zod.z.number().nullable().meta(readingMeta("none")),
69340
69362
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
69341
69363
  * single-hopper models. */
69342
- food2: zod.z.number().nullable(),
69364
+ food2: zod.z.number().nullable().meta(readingMeta("none")),
69343
69365
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
69344
69366
  * (`device_class: problem`, on = low). True when the bowl is empty /
69345
69367
  * below the feeder's low threshold. */
69346
69368
  lowFood: zod.z.boolean(),
69347
69369
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
69348
69370
  * device has no battery reading. */
69349
- batteryPower: zod.z.number().min(0).max(100).nullable(),
69350
- /** Days of desiccant life remaining. Null when the model has no
69351
- * desiccant sensor. */
69352
- desiccantLeftDays: zod.z.number().nullable(),
69371
+ batteryPower: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69372
+ /**
69373
+ * Desiccant life remaining, stored in seconds (duration base, D944).
69374
+ * Providers convert from the days the feeder reports. Null when the
69375
+ * model has no desiccant sensor.
69376
+ */
69377
+ desiccantLeftDays: zod.z.number().nullable().meta(readingMeta("duration")),
69353
69378
  /** True while a feed is in progress. */
69354
69379
  feeding: zod.z.boolean(),
69355
69380
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -69360,7 +69385,7 @@ var PetFeederStatusSchema = zod.z.object({
69360
69385
  * with `errorCode` for consumers that want the raw integer. */
69361
69386
  error: zod.z.string().nullable(),
69362
69387
  /** Raw device error code (0 / null = no error). */
69363
- errorCode: zod.z.number().nullable(),
69388
+ errorCode: zod.z.number().nullable().meta(readingMeta("none")),
69364
69389
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
69365
69390
  isDualHopper: zod.z.boolean(),
69366
69391
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -69370,7 +69395,7 @@ var PetFeederStatusSchema = zod.z.object({
69370
69395
  /** Play a chime when dispensing. */
69371
69396
  feedSound: zod.z.boolean(),
69372
69397
  /** Speaker / prompt volume level (device-scaled integer). */
69373
- volume: zod.z.number(),
69398
+ volume: zod.z.number().meta(readingMeta("none")),
69374
69399
  /** Ms epoch when the slice was last refreshed from the cloud. */
69375
69400
  lastFetchedAt: zod.z.number()
69376
69401
  });
@@ -69489,7 +69514,7 @@ var petFeederCapability = {
69489
69514
  },
69490
69515
  desiccantLeftDays: {
69491
69516
  kind: "gauge",
69492
- unit: "days"
69517
+ unit: "s"
69493
69518
  }
69494
69519
  } }
69495
69520
  };
@@ -69950,36 +69975,19 @@ var platformProbeCapability = {
69950
69975
  */
69951
69976
  var PowerMeterStatusSchema = zod.z.object({
69952
69977
  /** Instantaneous power draw in watts. */
69953
- watts: zod.z.number().optional().meta({
69954
- unit: "W",
69955
- label: "Power"
69956
- }),
69978
+ watts: zod.z.number().optional().meta(readingMeta("power", { label: "Power" })),
69957
69979
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
69958
- kwhTotal: zod.z.number().optional().meta({
69959
- unit: "kWh",
69960
- label: "Energy"
69961
- }),
69980
+ kwhTotal: zod.z.number().optional().meta(readingMeta("energy", { label: "Energy" })),
69962
69981
  /** Voltage in volts. */
69963
- volts: zod.z.number().optional(),
69982
+ volts: zod.z.number().optional().meta(readingMeta("voltage")),
69964
69983
  /** Current in amperes. */
69965
- amps: zod.z.number().optional(),
69966
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
69967
- va: zod.z.number().optional(),
69984
+ amps: zod.z.number().optional().meta(readingMeta("current")),
69985
+ /** Apparent power in volt-amperes. */
69986
+ va: zod.z.number().optional().meta(readingMeta("apparent-power")),
69968
69987
  /** Power factor (0..1) if reported. */
69969
- powerFactor: zod.z.number().min(0).max(1).optional(),
69988
+ powerFactor: zod.z.number().min(0).max(1).optional().meta(readingMeta("ratio")),
69970
69989
  /** Ms epoch when the slice was last updated. */
69971
- lastFetchedAt: zod.z.number(),
69972
- /** Live display unit of the single metric this slice carries (e.g. HA
69973
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
69974
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
69975
- * unit per slice is unambiguous. The UI prefers this over the role's
69976
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
69977
- unit: zod.z.string().optional(),
69978
- /** Suggested decimal places for numeric display.
69979
- * Populated live from the upstream source when provided (e.g. HA
69980
- * `attributes.suggested_display_precision`). Falls back to
69981
- * auto-formatting when absent. */
69982
- precision: zod.z.number().int().min(0).max(10).optional()
69990
+ lastFetchedAt: zod.z.number()
69983
69991
  });
69984
69992
  var powerMeterCapability = {
69985
69993
  name: "power-meter",
@@ -70043,12 +70051,7 @@ var powerMeterCapability = {
70043
70051
  } },
70044
70052
  /** Clock fields: written, but excluded from the compare that decides
70045
70053
  * whether persisting is worth a SQLite commit. */
70046
- volatileStateFields: ["lastFetchedAt"],
70047
- customize: {
70048
- version: 1,
70049
- advanced: "derive",
70050
- targets: []
70051
- }
70054
+ volatileStateFields: ["lastFetchedAt"]
70052
70055
  };
70053
70056
  /**
70054
70057
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -70068,11 +70071,11 @@ var powerMeterCapability = {
70068
70071
  */
70069
70072
  var GpsLocationSchema = zod.z.object({
70070
70073
  /** Latitude in decimal degrees, -90..90. */
70071
- latitude: zod.z.number().min(-90).max(90),
70074
+ latitude: zod.z.number().min(-90).max(90).meta(readingMeta("angle")),
70072
70075
  /** Longitude in decimal degrees, -180..180. */
70073
- longitude: zod.z.number().min(-180).max(180),
70076
+ longitude: zod.z.number().min(-180).max(180).meta(readingMeta("angle")),
70074
70077
  /** Reported accuracy in meters (lower = better). */
70075
- accuracyMeters: zod.z.number().nonnegative()
70078
+ accuracyMeters: zod.z.number().nonnegative().meta(readingMeta("length"))
70076
70079
  });
70077
70080
  var PresenceStatusSchema = zod.z.object({
70078
70081
  /** `home` / `not_home` / any user-defined zone name. */
@@ -70084,7 +70087,7 @@ var PresenceStatusSchema = zod.z.object({
70084
70087
  gps: GpsLocationSchema.nullable(),
70085
70088
  /** Optional battery level of the tracking device (0..100). Null
70086
70089
  * when not reported. */
70087
- batteryPercent: zod.z.number().min(0).max(100).nullable(),
70090
+ batteryPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
70088
70091
  /** Ms epoch when the slice was last updated. */
70089
70092
  lastChangedAt: zod.z.number()
70090
70093
  });
@@ -70134,19 +70137,10 @@ var presenceCapability = {
70134
70137
  * `sourceInfo`.
70135
70138
  */
70136
70139
  var PressureSensorStatusSchema = zod.z.object({
70137
- /** Current pressure in hPa. */
70138
- hpa: zod.z.number(),
70140
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
70141
+ hpa: zod.z.number().nullable().meta(readingMeta("pressure", { precision: 0 })),
70139
70142
  /** Ms epoch when the slice was last updated. */
70140
- lastFetchedAt: zod.z.number(),
70141
- /** Live display unit from the upstream source (e.g. HA
70142
- * `attributes.unit_of_measurement`). The UI prefers this over the
70143
- * role's canonical unit. Absent → fall back to the canonical unit. */
70144
- unit: zod.z.string().optional(),
70145
- /** Suggested decimal places for numeric display.
70146
- * Populated live from the upstream source when provided (e.g. HA
70147
- * `attributes.suggested_display_precision`). Falls back to
70148
- * auto-formatting when absent. */
70149
- precision: zod.z.number().int().min(0).max(10).optional()
70143
+ lastFetchedAt: zod.z.number()
70150
70144
  });
70151
70145
  var pressureSensorCapability = {
70152
70146
  name: "pressure-sensor",
@@ -70687,8 +70681,8 @@ var ptzCapability = {
70687
70681
  var AutotrackTargetTypeSchema = zod.z.string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
70688
70682
  var PtzAutotrackSettingsSchema = zod.z.object({
70689
70683
  targetType: AutotrackTargetTypeSchema,
70690
- stopDelaySeconds: zod.z.number().int().min(0).max(300),
70691
- disappearDelaySeconds: zod.z.number().int().min(0).max(300)
70684
+ stopDelaySeconds: zod.z.number().int().min(0).max(300).meta(readingMeta("duration")),
70685
+ disappearDelaySeconds: zod.z.number().int().min(0).max(300).meta(readingMeta("duration"))
70692
70686
  });
70693
70687
  /**
70694
70688
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -70842,7 +70836,7 @@ var RunningStateStatusSchema = zod.z.object({
70842
70836
  /** null = unknown — never coerced to false (D393). */
70843
70837
  running: zod.z.boolean().nullable(),
70844
70838
  /** Epoch ms the current boolean began; null while `running` is null. */
70845
- since: zod.z.number().nullable(),
70839
+ since: zod.z.number().nullable().meta(readingMeta("none")),
70846
70840
  source: RunningStateSourceSchema.meta({ label: "Source" }),
70847
70841
  phase: zod.z.string().nullable(),
70848
70842
  /** null until a reading has been published (D393). */
@@ -70857,7 +70851,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
70857
70851
  /** Generic Customize target for the running boolean (D936). */
70858
70852
  var runningCustomize = {
70859
70853
  version: 1,
70860
- advanced: "derive",
70861
70854
  targets: [{
70862
70855
  id: "running",
70863
70856
  cardinality: "scalar",
@@ -70877,7 +70870,7 @@ var runningCustomize = {
70877
70870
  DeviceType.Thermostat,
70878
70871
  DeviceType.PetFeeder
70879
70872
  ],
70880
- apply: "add-or-replace",
70873
+ apply: "add",
70881
70874
  path: "running",
70882
70875
  sources: [
70883
70876
  {
@@ -72516,9 +72509,9 @@ var RecordTriggerSchema = zod.z.enum([
72516
72509
  */
72517
72510
  var RecordWindowSchema = zod.z.object({
72518
72511
  trigger: RecordTriggerSchema,
72519
- day: zod.z.number().int().min(0).max(6),
72520
- startMinute: zod.z.number().int().min(0).max(1439),
72521
- endMinute: zod.z.number().int().min(1).max(1440)
72512
+ day: zod.z.number().int().min(0).max(6).meta(readingMeta("none")),
72513
+ startMinute: zod.z.number().int().min(0).max(1439).meta(readingMeta("none")),
72514
+ endMinute: zod.z.number().int().min(1).max(1440).meta(readingMeta("none"))
72522
72515
  });
72523
72516
  /** Status of one physical volume, as the camera itself describes it. */
72524
72517
  var OnboardStorageVolumeSchema = zod.z.object({
@@ -72617,10 +72610,10 @@ var RecordingOnboardStatusSchema = zod.z.object({
72617
72610
  /** Master "record to the card at all" switch. */
72618
72611
  enabled: zod.z.boolean().nullable(),
72619
72612
  overwriteWhenFull: zod.z.boolean().nullable(),
72620
- preRecordSec: zod.z.number().nullable(),
72621
- postRecordSec: zod.z.number().nullable(),
72613
+ preRecordSec: zod.z.number().nullable().meta(readingMeta("duration")),
72614
+ postRecordSec: zod.z.number().nullable().meta(readingMeta("duration")),
72622
72615
  /** Length of one recorded file, in minutes. */
72623
- segmentMinutes: zod.z.number().nullable(),
72616
+ segmentMinutes: zod.z.number().nullable().meta(readingMeta("none")),
72624
72617
  /** The primary track's weekly windows, flattened. */
72625
72618
  windows: zod.z.array(RecordWindowSchema),
72626
72619
  /**
@@ -72636,7 +72629,7 @@ var RecordingOnboardStatusSchema = zod.z.object({
72636
72629
  * provider reporting a non-zero count also reports the schedule as not
72637
72630
  * writable.
72638
72631
  */
72639
- unreadableWindows: zod.z.number(),
72632
+ unreadableWindows: zod.z.number().meta(readingMeta("count")),
72640
72633
  /**
72641
72634
  * The camera is scheduled to record and has NO usable storage.
72642
72635
  *
@@ -73502,7 +73495,7 @@ var CamStreamDescriptorSchema = zod.z.object({
73502
73495
  url: zod.z.string().optional(),
73503
73496
  codec: zod.z.string().optional(),
73504
73497
  resolution: CamStreamResolutionSchema.optional(),
73505
- fps: zod.z.number().positive().optional(),
73498
+ fps: zod.z.number().positive().optional().meta(readingMeta("rate")),
73506
73499
  label: zod.z.string().optional(),
73507
73500
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
73508
73501
  deviceFeatures: zod.z.array(zod.z.string()).optional(),
@@ -73589,18 +73582,18 @@ var StreamProfileSchema = zod.z.enum([
73589
73582
  ]);
73590
73583
  /** The current encoding config of one stream profile. */
73591
73584
  var StreamProfileConfigSchema = zod.z.object({
73592
- width: zod.z.number(),
73593
- height: zod.z.number(),
73585
+ width: zod.z.number().meta(readingMeta("none")),
73586
+ height: zod.z.number().meta(readingMeta("none")),
73594
73587
  codec: zod.z.enum(["h264", "h265"]),
73595
- framerate: zod.z.number(),
73596
- bitrate: zod.z.number(),
73588
+ framerate: zod.z.number().meta(readingMeta("rate")),
73589
+ bitrate: zod.z.number().meta(readingMeta("bitrate")),
73597
73590
  bitrateMode: zod.z.enum(["vbr", "cbr"]).optional(),
73598
73591
  encoderProfile: zod.z.enum([
73599
73592
  "high",
73600
73593
  "main",
73601
73594
  "baseline"
73602
73595
  ]).optional(),
73603
- gop: zod.z.number().optional(),
73596
+ gop: zod.z.number().optional().meta(readingMeta("none")),
73604
73597
  /**
73605
73598
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
73606
73599
  *
@@ -74078,12 +74071,7 @@ var switchCapability = {
74078
74071
  label: "On",
74079
74072
  kind: "boolean"
74080
74073
  }
74081
- }] },
74082
- customize: {
74083
- version: 1,
74084
- advanced: "derive",
74085
- targets: []
74086
- }
74074
+ }] }
74087
74075
  };
74088
74076
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
74089
74077
  function hasControlCharacter(value) {
@@ -74793,35 +74781,21 @@ var tamperCapability = {
74793
74781
  * entries with `device_class: temperature` and any future native
74794
74782
  * thermometer.
74795
74783
  *
74796
- * Unit normalisation: providers convert to Celsius before storing.
74797
- * The slice value is always Celsius so cross-cap aggregators
74798
- * (climate-control's `currentTemp`, energy analytics) can compose
74799
- * without per-source unit fixups. The canonical display unit (`°C`) is
74800
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
74801
- * `sourceInfo`.
74784
+ * The number is Celsius (D944). Providers convert on ingest. A failed
74785
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
74802
74786
  *
74803
74787
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
74804
74788
  * frozen feed (provider hung) distinct from a "temperature hasn't
74805
74789
  * changed" steady state.
74806
74790
  */
74807
74791
  var TemperatureSensorStatusSchema = zod.z.object({
74808
- /** Current temperature in Celsius. */
74809
- celsius: zod.z.number().meta({
74810
- unit: "°C",
74792
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
74793
+ celsius: zod.z.number().nullable().meta(readingMeta("temperature", {
74794
+ precision: 1,
74811
74795
  label: "Temperature"
74812
- }),
74796
+ })),
74813
74797
  /** Ms epoch when the slice was last updated (push or poll). */
74814
- lastFetchedAt: zod.z.number(),
74815
- /** Live display unit from the upstream source (e.g. HA
74816
- * `attributes.unit_of_measurement`). The UI prefers this over the
74817
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
74818
- * Absent → the UI falls back to the role's canonical unit. */
74819
- unit: zod.z.string().optional(),
74820
- /** Suggested decimal places for numeric display.
74821
- * Populated live from the upstream source when provided (e.g. HA
74822
- * `attributes.suggested_display_precision`). Falls back to
74823
- * auto-formatting when absent. */
74824
- precision: zod.z.number().int().min(0).max(10).optional()
74798
+ lastFetchedAt: zod.z.number()
74825
74799
  });
74826
74800
  var temperatureSensorCapability = {
74827
74801
  name: "temperature-sensor",
@@ -74852,12 +74826,7 @@ var temperatureSensorCapability = {
74852
74826
  } } },
74853
74827
  /** Clock fields: written, but excluded from the compare that decides
74854
74828
  * whether persisting is worth a SQLite commit. */
74855
- volatileStateFields: ["lastFetchedAt"],
74856
- customize: {
74857
- version: 1,
74858
- advanced: "derive",
74859
- targets: []
74860
- }
74829
+ volatileStateFields: ["lastFetchedAt"]
74861
74830
  };
74862
74831
  /**
74863
74832
  * toast — system-scoped singleton capability that streams toast
@@ -75203,16 +75172,16 @@ var VacuumControlStatusSchema = zod.z.object({
75203
75172
  state: VacuumStateSchema,
75204
75173
  /** 0..100 battery percentage. Null when the device has no battery
75205
75174
  * reading. */
75206
- batteryLevel: zod.z.number().min(0).max(100).nullable(),
75175
+ batteryLevel: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
75207
75176
  /** Current fan-speed token (provider-verbatim). Null when unknown or
75208
75177
  * the vacuum has no speed control. */
75209
75178
  fanSpeed: zod.z.string().nullable(),
75210
75179
  /** Speed tokens the hardware accepts — drives the UI selector. */
75211
75180
  availableFanSpeeds: zod.z.array(zod.z.string()),
75212
75181
  /** 0..100 cleaning-completion percentage of the current task, or null. */
75213
- progressPercent: zod.z.number().min(0).max(100).nullable(),
75182
+ progressPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
75214
75183
  /** Current error code (0 / null = no error). */
75215
- errorCode: zod.z.number().nullable(),
75184
+ errorCode: zod.z.number().nullable().meta(readingMeta("none")),
75216
75185
  /** Human label for {@link errorCode}, or null when none / undecodable. */
75217
75186
  errorLabel: zod.z.string().nullable(),
75218
75187
  /** Ms epoch when the slice was last updated. */
@@ -75308,7 +75277,7 @@ var ValveStatusSchema = zod.z.object({
75308
75277
  state: ValveStateSchema,
75309
75278
  /** 0 = fully closed, 100 = fully open. Null when the device has no
75310
75279
  * intermediate position surface. */
75311
- position: zod.z.number().min(0).max(100).nullable(),
75280
+ position: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
75312
75281
  /** Ms epoch when the slice was last updated. */
75313
75282
  lastChangedAt: zod.z.number()
75314
75283
  });
@@ -75416,9 +75385,9 @@ var vibrationCapability = {
75416
75385
  */
75417
75386
  var WaterHeaterStatusSchema = zod.z.object({
75418
75387
  /** Current measured temperature. Null when not reported. */
75419
- currentTemp: zod.z.number().nullable(),
75388
+ currentTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75420
75389
  /** Target temperature setpoint. Null when no setpoint surface. */
75421
- targetTemp: zod.z.number().nullable(),
75390
+ targetTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75422
75391
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
75423
75392
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
75424
75393
  * device reports an unknown state. */
@@ -75429,9 +75398,9 @@ var WaterHeaterStatusSchema = zod.z.object({
75429
75398
  * has no away surface. */
75430
75399
  away: zod.z.boolean().nullable(),
75431
75400
  /** HA `min_temp` attribute. Null when not reported. */
75432
- minTemp: zod.z.number().nullable(),
75401
+ minTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75433
75402
  /** HA `max_temp` attribute. Null when not reported. */
75434
- maxTemp: zod.z.number().nullable(),
75403
+ maxTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75435
75404
  /** Ms epoch when the slice was last updated. */
75436
75405
  lastChangedAt: zod.z.number()
75437
75406
  });
@@ -75516,25 +75485,18 @@ var waterHeaterCapability = {
75516
75485
  * Forecast deferred — current conditions only for v1.
75517
75486
  */
75518
75487
  var WeatherStatusSchema = zod.z.object({
75519
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
75520
- * when no condition has been reported yet. */
75488
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
75521
75489
  condition: zod.z.string().nullable(),
75522
- /** Current temperature in the reported unit. Null when not provided. */
75523
- temperature: zod.z.number().nullable(),
75524
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
75525
- temperatureUnit: zod.z.string().nullable(),
75490
+ /** Current temperature in Celsius. Null when not provided or refused. */
75491
+ temperature: zod.z.number().nullable().meta(readingMeta("temperature", { precision: 1 })),
75526
75492
  /** Relative humidity (0..100). Null when not provided. */
75527
- humidity: zod.z.number().min(0).max(100).nullable(),
75528
- /** Barometric pressure in the reported unit. Null when not provided. */
75529
- pressure: zod.z.number().nullable(),
75530
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
75531
- pressureUnit: zod.z.string().nullable(),
75532
- /** Wind speed in the reported unit. Null when not provided. */
75533
- windSpeed: zod.z.number().nullable(),
75534
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
75535
- windSpeedUnit: zod.z.string().nullable(),
75493
+ humidity: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity")),
75494
+ /** Barometric pressure in hPa. Null when not provided or refused. */
75495
+ pressure: zod.z.number().nullable().meta(readingMeta("pressure")),
75496
+ /** Wind speed in metres per second. Null when not provided or refused. */
75497
+ windSpeed: zod.z.number().nullable().meta(readingMeta("speed")),
75536
75498
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
75537
- windBearing: zod.z.number().nullable(),
75499
+ windBearing: zod.z.number().nullable().meta(readingMeta("angle")),
75538
75500
  /** Ms epoch when the slice was last updated. */
75539
75501
  lastFetchedAt: zod.z.number()
75540
75502
  });
@@ -75562,12 +75524,12 @@ var weatherCapability = {
75562
75524
  */
75563
75525
  durability: "session",
75564
75526
  /**
75565
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
75527
+ * History (D850): recorded series. Numbers are the quantity base (D944).
75566
75528
  */
75567
75529
  history: { fields: {
75568
75530
  temperature: {
75569
75531
  kind: "gauge",
75570
- unitFrom: "temperatureUnit",
75532
+ unit: "°C",
75571
75533
  deadband: .2
75572
75534
  },
75573
75535
  humidity: {
@@ -75577,12 +75539,12 @@ var weatherCapability = {
75577
75539
  },
75578
75540
  pressure: {
75579
75541
  kind: "gauge",
75580
- unitFrom: "pressureUnit",
75542
+ unit: "hPa",
75581
75543
  deadband: .5
75582
75544
  },
75583
75545
  windSpeed: {
75584
75546
  kind: "gauge",
75585
- unitFrom: "windSpeedUnit",
75547
+ unit: "m/s",
75586
75548
  deadband: .5
75587
75549
  }
75588
75550
  } }
@@ -76793,6 +76755,11 @@ var UNIT_TABLE = {
76793
76755
  factor: 1609.344,
76794
76756
  offset: 0
76795
76757
  },
76758
+ "m²": {
76759
+ dimension: "area",
76760
+ factor: 1,
76761
+ offset: 0
76762
+ },
76796
76763
  lx: {
76797
76764
  dimension: "illuminance",
76798
76765
  factor: 1,
@@ -76897,6 +76864,121 @@ var UNIT_TABLE = {
76897
76864
  dimension: "percentage",
76898
76865
  factor: 1,
76899
76866
  offset: 0
76867
+ },
76868
+ L: {
76869
+ dimension: "volume",
76870
+ factor: 1,
76871
+ offset: 0
76872
+ },
76873
+ mL: {
76874
+ dimension: "volume",
76875
+ factor: .001,
76876
+ offset: 0
76877
+ },
76878
+ gal: {
76879
+ dimension: "volume",
76880
+ factor: 3.785411784,
76881
+ offset: 0
76882
+ },
76883
+ s: {
76884
+ dimension: "duration",
76885
+ factor: 1,
76886
+ offset: 0
76887
+ },
76888
+ ms: {
76889
+ dimension: "duration",
76890
+ factor: .001,
76891
+ offset: 0
76892
+ },
76893
+ min: {
76894
+ dimension: "duration",
76895
+ factor: 60,
76896
+ offset: 0
76897
+ },
76898
+ h: {
76899
+ dimension: "duration",
76900
+ factor: 3600,
76901
+ offset: 0
76902
+ },
76903
+ d: {
76904
+ dimension: "duration",
76905
+ factor: 86400,
76906
+ offset: 0
76907
+ },
76908
+ B: {
76909
+ dimension: "information",
76910
+ factor: 1,
76911
+ offset: 0
76912
+ },
76913
+ KiB: {
76914
+ dimension: "information",
76915
+ factor: 1024,
76916
+ offset: 0
76917
+ },
76918
+ MiB: {
76919
+ dimension: "information",
76920
+ factor: 1024 ** 2,
76921
+ offset: 0
76922
+ },
76923
+ GiB: {
76924
+ dimension: "information",
76925
+ factor: 1024 ** 3,
76926
+ offset: 0
76927
+ },
76928
+ TiB: {
76929
+ dimension: "information",
76930
+ factor: 1024 ** 4,
76931
+ offset: 0
76932
+ },
76933
+ kB: {
76934
+ dimension: "information",
76935
+ factor: 1e3,
76936
+ offset: 0
76937
+ },
76938
+ MB: {
76939
+ dimension: "information",
76940
+ factor: 1e6,
76941
+ offset: 0
76942
+ },
76943
+ GB: {
76944
+ dimension: "information",
76945
+ factor: 1e9,
76946
+ offset: 0
76947
+ },
76948
+ "°": {
76949
+ dimension: "angle",
76950
+ factor: 1,
76951
+ offset: 0
76952
+ },
76953
+ dBFS: {
76954
+ dimension: "sound-level",
76955
+ factor: 1,
76956
+ offset: 0
76957
+ },
76958
+ dBm: {
76959
+ dimension: "signal-level",
76960
+ factor: 1,
76961
+ offset: 0
76962
+ },
76963
+ mired: {
76964
+ dimension: "colour-temperature",
76965
+ factor: 1,
76966
+ offset: 0
76967
+ },
76968
+ kbps: {
76969
+ dimension: "bitrate",
76970
+ factor: 1,
76971
+ offset: 0
76972
+ },
76973
+ Mbps: {
76974
+ dimension: "bitrate",
76975
+ factor: 1e3,
76976
+ offset: 0
76977
+ },
76978
+ fps: {
76979
+ dimension: "rate",
76980
+ factor: 1,
76981
+ offset: 0
76900
76982
  }
76901
76983
  };
76902
76984
  /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
@@ -91519,7 +91601,6 @@ var SENSOR_CAP_VOCABULARY = {
91519
91601
  field: "value",
91520
91602
  quantity: t("the reading", "il valore"),
91521
91603
  label: t("Reading", "Valore"),
91522
- unitField: "unit",
91523
91604
  icon: "gauge"
91524
91605
  }]
91525
91606
  },
@@ -91655,7 +91736,7 @@ var SENSOR_CAP_VOCABULARY = {
91655
91736
  field: "temperature",
91656
91737
  quantity: t("the outdoor temperature", "la temperatura esterna"),
91657
91738
  label: t("Temperature", "Temperatura"),
91658
- unitField: "temperatureUnit",
91739
+ unit: "°C",
91659
91740
  icon: "thermometer"
91660
91741
  },
91661
91742
  {
@@ -91671,7 +91752,7 @@ var SENSOR_CAP_VOCABULARY = {
91671
91752
  field: "windSpeed",
91672
91753
  quantity: t("the wind speed", "la velocità del vento"),
91673
91754
  label: t("Wind", "Vento"),
91674
- unitField: "windSpeedUnit",
91755
+ unit: "m/s",
91675
91756
  icon: "wind"
91676
91757
  },
91677
91758
  {
@@ -91679,7 +91760,7 @@ var SENSOR_CAP_VOCABULARY = {
91679
91760
  field: "pressure",
91680
91761
  quantity: t("the air pressure", "la pressione atmosferica"),
91681
91762
  label: t("Pressure", "Pressione"),
91682
- unitField: "pressureUnit",
91763
+ unit: "hPa",
91683
91764
  icon: "gauge"
91684
91765
  }
91685
91766
  ]
@@ -95347,7 +95428,7 @@ var SESSION_ONLY_POLICY = {
95347
95428
  };
95348
95429
  /**
95349
95430
  * Cap history catalog, projected from every `*.cap.ts` `history` block.
95350
- * 41 caps with series (74 series), 30 caps answered `'none'`.
95431
+ * 41 caps with series (75 series), 30 caps answered `'none'`.
95351
95432
  * Field keys are `runtimeState` paths (`a.b` into fixed objects only).
95352
95433
  */
95353
95434
  var HISTORY_CATALOG = {
@@ -95364,12 +95445,12 @@ var HISTORY_CATALOG = {
95364
95445
  },
95365
95446
  "pm25": {
95366
95447
  kind: "gauge",
95367
- unit: "ug/m3",
95448
+ unit: "µg/m³",
95368
95449
  deadband: 1
95369
95450
  },
95370
95451
  "pm10": {
95371
95452
  kind: "gauge",
95372
- unit: "ug/m3",
95453
+ unit: "µg/m³",
95373
95454
  deadband: 1
95374
95455
  },
95375
95456
  "aqi": {
@@ -95399,10 +95480,16 @@ var HISTORY_CATALOG = {
95399
95480
  deadbandPct: 10,
95400
95481
  minIntervalMs: 6e4
95401
95482
  } },
95402
- "battery": { "percentage": {
95403
- kind: "gauge",
95404
- unit: "%"
95405
- } },
95483
+ "battery": {
95484
+ "percentage": {
95485
+ kind: "gauge",
95486
+ unit: "%"
95487
+ },
95488
+ "volts": {
95489
+ kind: "gauge",
95490
+ unit: "V"
95491
+ }
95492
+ },
95406
95493
  "binary": { "on": { kind: "boolean" } },
95407
95494
  "brightness": { "percentage": {
95408
95495
  kind: "gauge",
@@ -95486,7 +95573,7 @@ var HISTORY_CATALOG = {
95486
95573
  },
95487
95574
  "enum-sensor": { "value": {
95488
95575
  kind: "enum",
95489
- skipWhenSet: "format"
95576
+ skipWhenSet: "timestamp"
95490
95577
  } },
95491
95578
  "fan-control": {
95492
95579
  "percentage": {
@@ -95559,7 +95646,6 @@ var HISTORY_CATALOG = {
95559
95646
  },
95560
95647
  "numeric-sensor": { "value": {
95561
95648
  kind: "gauge",
95562
- unitFrom: "unit",
95563
95649
  minIntervalMs: 3e4
95564
95650
  } },
95565
95651
  "pet-feeder": {
@@ -95573,7 +95659,7 @@ var HISTORY_CATALOG = {
95573
95659
  },
95574
95660
  "desiccantLeftDays": {
95575
95661
  kind: "gauge",
95576
- unit: "days"
95662
+ unit: "s"
95577
95663
  }
95578
95664
  },
95579
95665
  "power-meter": {
@@ -95677,7 +95763,7 @@ var HISTORY_CATALOG = {
95677
95763
  "weather": {
95678
95764
  "temperature": {
95679
95765
  kind: "gauge",
95680
- unitFrom: "temperatureUnit",
95766
+ unit: "°C",
95681
95767
  deadband: .2
95682
95768
  },
95683
95769
  "humidity": {
@@ -95687,12 +95773,12 @@ var HISTORY_CATALOG = {
95687
95773
  },
95688
95774
  "pressure": {
95689
95775
  kind: "gauge",
95690
- unitFrom: "pressureUnit",
95776
+ unit: "hPa",
95691
95777
  deadband: .5
95692
95778
  },
95693
95779
  "windSpeed": {
95694
95780
  kind: "gauge",
95695
- unitFrom: "windSpeedUnit",
95781
+ unit: "m/s",
95696
95782
  deadband: .5
95697
95783
  }
95698
95784
  }
@@ -95726,7 +95812,6 @@ var HISTORY_ITEM_CATALOG = { "consumables": {
95726
95812
  },
95727
95813
  "remaining.value": {
95728
95814
  kind: "gauge",
95729
- unitFrom: "remaining.unit",
95730
95815
  deadband: 1,
95731
95816
  displayName: "Remaining"
95732
95817
  }
@@ -98620,6 +98705,12 @@ Object.defineProperty(exports, "historyItemFieldPath", {
98620
98705
  return historyItemFieldPath;
98621
98706
  }
98622
98707
  });
98708
+ Object.defineProperty(exports, "historyUnitFor", {
98709
+ enumerable: true,
98710
+ get: function() {
98711
+ return historyUnitFor;
98712
+ }
98713
+ });
98623
98714
  Object.defineProperty(exports, "hydrateSchema", {
98624
98715
  enumerable: true,
98625
98716
  get: function() {
@@ -98704,6 +98795,12 @@ Object.defineProperty(exports, "isObjectInput", {
98704
98795
  return isObjectInput;
98705
98796
  }
98706
98797
  });
98798
+ Object.defineProperty(exports, "isReadingQuantity", {
98799
+ enumerable: true,
98800
+ get: function() {
98801
+ return isReadingQuantity;
98802
+ }
98803
+ });
98707
98804
  Object.defineProperty(exports, "isSecretClear", {
98708
98805
  enumerable: true,
98709
98806
  get: function() {