@camstack/system 1.2.483 → 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-CHZpVx3I.js → agent-https-DBP7P-ee.js} +1 -1
  2. package/dist/{agent-https-NP141VCW.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 +280 -280
  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-CQxrCexr.js → composition-sources-BSQneZV9.js} +1 -1
  68. package/dist/{composition-sources-CC13wGKL.mjs → composition-sources-BnZpIA8u.mjs} +1 -1
  69. package/dist/{constants-DMB-ex5z.mjs → constants-7D7xjw0h.mjs} +1 -1
  70. package/dist/{constants-Bt0NFXDm.js → constants-CQfTeCnB.js} +1 -1
  71. package/dist/{device-ca-HQKcvofS.mjs → device-ca-Bzd6UO_c.mjs} +1 -1
  72. package/dist/{device-ca-CSLMfr4Q.js → device-ca-D_4dI5gv.js} +1 -1
  73. package/dist/{dist-DvKEKSap.mjs → dist-Df6c0RX_.mjs} +614 -522
  74. package/dist/{dist-2gZWO597.js → dist-xCNJAQMh.js} +625 -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-CVf6mxIZ.mjs → hub-mdns-ciao-D7GMnEz0.mjs} +2 -2
  78. package/dist/{hub-mdns-ciao-Bs3cn9C1.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-EFYeCdrG.mjs → restore-archive-Co70oRya.mjs} +78 -78
  82. package/dist/{restore-archive-_mgadWlk.js → restore-archive-DA7N6ZZA.js} +1 -1
  83. package/dist/{retention-qugQGYuY.js → retention-DNDOPW7Z.js} +1 -1
  84. package/dist/{retention-FFJW3-3P.mjs → retention-DfCW-dZS.mjs} +1 -1
  85. package/dist/{wal-maintenance-TCW_esAc.mjs → wal-maintenance-71-j48Ci.mjs} +0 -0
  86. package/dist/{wal-maintenance-DbUC_wdC.js → wal-maintenance-CA0L6WZ9.js} +0 -0
  87. package/package.json +7 -2
@@ -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. */
@@ -64742,6 +64798,7 @@ var doorbellCustomize = {
64742
64798
  applyAuthority: { kind: "composition-claim" },
64743
64799
  eventName: "onPressed",
64744
64800
  edges: ["rising"],
64801
+ defaultEdge: "rising",
64745
64802
  timestampPath: "lastPressedAt",
64746
64803
  counterPath: "pressCountSinceStart",
64747
64804
  highWater: "composer-advance",
@@ -64760,7 +64817,7 @@ var DoorbellStatusSchema = zod.z.object({
64760
64817
  /** Ms epoch of the last press. null = never observed since this provider started. */
64761
64818
  lastPressedAt: zod.z.number().nullable(),
64762
64819
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
64763
- pressCountSinceStart: zod.z.number()
64820
+ pressCountSinceStart: zod.z.number().meta(readingMeta("count"))
64764
64821
  });
64765
64822
  var DoorbellPressEventSchema = zod.z.object({
64766
64823
  deviceId: zod.z.number(),
@@ -64825,17 +64882,7 @@ onPressed: {
64825
64882
  history: "none",
64826
64883
  customize: doorbellCustomize
64827
64884
  };
64828
- /**
64829
- * Enum-state sensor — a string value picked from a finite option set.
64830
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
64831
- * states, weather conditions, contact-source channels, mode strings).
64832
- * The option set is stable across observations; it is persisted in the
64833
- * device config blob (via the control slice at adoption) and is NOT
64834
- * stored in `sourceInfo`.
64835
- */
64836
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
64837
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
64838
- var EnumSensorDateTimeFormatSchema = zod.z.enum([
64885
+ zod.z.enum([
64839
64886
  "date",
64840
64887
  "time",
64841
64888
  "datetime"
@@ -64843,12 +64890,11 @@ var EnumSensorDateTimeFormatSchema = zod.z.enum([
64843
64890
  var EnumSensorStatusSchema = zod.z.object({
64844
64891
  value: zod.z.string(),
64845
64892
  /**
64846
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
64847
- * as a locale date/time/datetime; absent for plain enum sensors. HA
64848
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
64849
- * 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.
64850
64896
  */
64851
- format: EnumSensorDateTimeFormatSchema.optional(),
64897
+ timestamp: zod.z.boolean().optional(),
64852
64898
  /** Ms epoch when the slice was last updated. */
64853
64899
  lastFetchedAt: zod.z.number()
64854
64900
  });
@@ -64875,11 +64921,11 @@ var enumSensorCapability = {
64875
64921
  * History (D850): the value as an `enum` series (an appliance's program or
64876
64922
  * operation state, an HVAC action). The option set is per device, so the
64877
64923
  * value list is LEARNED as values are first seen (bounded). A sensor with a
64878
- * `format` carries a timestamp, not a state, and is not recorded.
64924
+ * `timestamp` carries a clock, not a state, and is not recorded.
64879
64925
  */
64880
64926
  history: { fields: { value: {
64881
64927
  kind: "enum",
64882
- skipWhenSet: "format"
64928
+ skipWhenSet: "timestamp"
64883
64929
  } } },
64884
64930
  /** Clock fields: written, but excluded from the compare that decides
64885
64931
  * whether persisting is worth a SQLite commit. */
@@ -64899,13 +64945,13 @@ var EventFireSchema = zod.z.object({
64899
64945
  deviceId: zod.z.number(),
64900
64946
  eventType: zod.z.string(),
64901
64947
  data: zod.z.record(zod.z.string(), zod.z.unknown()).nullable(),
64902
- timestamp: zod.z.number(),
64903
- seq: zod.z.number()
64948
+ timestamp: zod.z.number().meta(readingMeta("none")),
64949
+ seq: zod.z.number().meta(readingMeta("count"))
64904
64950
  });
64905
64951
  var EventEmitterStatusSchema = zod.z.object({
64906
64952
  eventTypes: zod.z.array(zod.z.string()),
64907
64953
  lastEvent: EventFireSchema.nullable(),
64908
- eventCountSinceStart: zod.z.number()
64954
+ eventCountSinceStart: zod.z.number().meta(readingMeta("count"))
64909
64955
  });
64910
64956
  var eventEmitterCapability = {
64911
64957
  name: "event-emitter",
@@ -65358,10 +65404,10 @@ var FanDirectionSchema = zod.z.enum(["forward", "reverse"]);
65358
65404
  var FanControlStatusSchema = zod.z.object({
65359
65405
  /** Active speed as 0..100 inclusive. Null when the device has no
65360
65406
  * speed surface (single-speed fan). */
65361
- percentage: zod.z.number().min(0).max(100).nullable(),
65407
+ percentage: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65362
65408
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
65363
65409
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
65364
- percentageStep: zod.z.number().positive().optional(),
65410
+ percentageStep: zod.z.number().positive().optional().meta(readingMeta("percentage")),
65365
65411
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
65366
65412
  * — empty string when no preset is active or supported. */
65367
65413
  preset: zod.z.string(),
@@ -65482,7 +65528,7 @@ var FeatureProbeStatusSchema = zod.z.object({
65482
65528
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
65483
65529
  model: zod.z.string().nullable(),
65484
65530
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
65485
- channelCount: zod.z.number().nullable(),
65531
+ channelCount: zod.z.number().nullable().meta(readingMeta("count")),
65486
65532
  /**
65487
65533
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
65488
65534
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -65607,7 +65653,7 @@ var gasCapability = {
65607
65653
  };
65608
65654
  var HeatingValveStatusSchema = zod.z.object({
65609
65655
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
65610
- position: zod.z.number().min(0).max(100).nullable(),
65656
+ position: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65611
65657
  /** Ms epoch when the slice was last updated (push or poll). */
65612
65658
  lastFetchedAt: zod.z.number()
65613
65659
  });
@@ -65650,9 +65696,9 @@ var HumidifierStatusSchema = zod.z.object({
65650
65696
  /** Whether the humidifier is currently on. */
65651
65697
  on: zod.z.boolean(),
65652
65698
  /** Current measured relative humidity (0..100). Null when not reported. */
65653
- currentHumidity: zod.z.number().min(0).max(100).nullable(),
65699
+ currentHumidity: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65654
65700
  /** Target relative humidity (0..100). Null when no setpoint surface. */
65655
- targetHumidity: zod.z.number().min(0).max(100).nullable(),
65701
+ targetHumidity: zod.z.number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65656
65702
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
65657
65703
  * device has no mode surface. */
65658
65704
  mode: zod.z.string().nullable(),
@@ -65662,9 +65708,9 @@ var HumidifierStatusSchema = zod.z.object({
65662
65708
  * `idle` / `off`). Null when the device doesn't report it. */
65663
65709
  action: zod.z.string().nullable(),
65664
65710
  /** HA `min_humidity` attribute. Null → UI uses 0. */
65665
- minHumidity: zod.z.number().nullable(),
65711
+ minHumidity: zod.z.number().nullable().meta(readingMeta("humidity")),
65666
65712
  /** HA `max_humidity` attribute. Null → UI uses 100. */
65667
- maxHumidity: zod.z.number().nullable(),
65713
+ maxHumidity: zod.z.number().nullable().meta(readingMeta("humidity")),
65668
65714
  /** Ms epoch when the slice was last updated. */
65669
65715
  lastChangedAt: zod.z.number()
65670
65716
  });
@@ -65734,22 +65780,13 @@ var humidifierCapability = {
65734
65780
  * `sourceInfo`.
65735
65781
  */
65736
65782
  var HumiditySensorStatusSchema = zod.z.object({
65737
- /** Current relative humidity, 0..100. */
65738
- percent: zod.z.number().min(0).max(100).meta({
65739
- 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,
65740
65786
  label: "Humidity"
65741
- }),
65787
+ })),
65742
65788
  /** Ms epoch when the slice was last updated. */
65743
- lastFetchedAt: zod.z.number(),
65744
- /** Live display unit from the upstream source (e.g. HA
65745
- * `attributes.unit_of_measurement`). The UI prefers this over the
65746
- * role's canonical unit. Absent → fall back to the canonical unit. */
65747
- unit: zod.z.string().optional(),
65748
- /** Suggested decimal places for numeric display.
65749
- * Populated live from the upstream source when provided (e.g. HA
65750
- * `attributes.suggested_display_precision`). Falls back to
65751
- * auto-formatting when absent. */
65752
- precision: zod.z.number().int().min(0).max(10).optional()
65789
+ lastFetchedAt: zod.z.number()
65753
65790
  });
65754
65791
  var humiditySensorCapability = {
65755
65792
  name: "humidity-sensor",
@@ -65780,12 +65817,7 @@ var humiditySensorCapability = {
65780
65817
  } } },
65781
65818
  /** Clock fields: written, but excluded from the compare that decides
65782
65819
  * whether persisting is worth a SQLite commit. */
65783
- volatileStateFields: ["lastFetchedAt"],
65784
- customize: {
65785
- version: 1,
65786
- advanced: "derive",
65787
- targets: []
65788
- }
65820
+ volatileStateFields: ["lastFetchedAt"]
65789
65821
  };
65790
65822
  /**
65791
65823
  * Image display cap. Models a single still image exposed by an integration —
@@ -65806,7 +65838,7 @@ var ImageStatusSchema = zod.z.object({
65806
65838
  * entity exposes no `entity_picture` (yet). */
65807
65839
  url: zod.z.string().nullable(),
65808
65840
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
65809
- lastUpdated: zod.z.number().nullable()
65841
+ lastUpdated: zod.z.number().nullable().meta(readingMeta("none"))
65810
65842
  });
65811
65843
  var imageCapability = {
65812
65844
  name: "image",
@@ -65894,19 +65926,19 @@ var NormalizedRangeSchema = zod.z.object({
65894
65926
  */
65895
65927
  var ImageSettingsStatusSchema = zod.z.object({
65896
65928
  /** Normalized 0–100. */
65897
- brightness: zod.z.number().optional(),
65929
+ brightness: zod.z.number().optional().meta(readingMeta("none")),
65898
65930
  /** Normalized 0–100. */
65899
- contrast: zod.z.number().optional(),
65931
+ contrast: zod.z.number().optional().meta(readingMeta("none")),
65900
65932
  /** Normalized 0–100. */
65901
- saturation: zod.z.number().optional(),
65933
+ saturation: zod.z.number().optional().meta(readingMeta("none")),
65902
65934
  /** Normalized 0–100. */
65903
- sharpness: zod.z.number().optional(),
65935
+ sharpness: zod.z.number().optional().meta(readingMeta("none")),
65904
65936
  mirror: zod.z.boolean().optional(),
65905
65937
  flip: zod.z.boolean().optional(),
65906
65938
  rotate: ImageRotateSchema.optional(),
65907
65939
  whiteBalance: WhiteBalanceModeSchema.optional(),
65908
65940
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
65909
- warmth: zod.z.number().optional(),
65941
+ warmth: zod.z.number().optional().meta(readingMeta("none")),
65910
65942
  exposureMode: ExposureModeSchema.optional(),
65911
65943
  backlightMode: BacklightModeSchema.optional(),
65912
65944
  lastFetchedAt: zod.z.number()
@@ -66203,10 +66235,10 @@ var IntercomAbilitySchema = zod.z.object({
66203
66235
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
66204
66236
  codecs: zod.z.array(zod.z.string()),
66205
66237
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
66206
- sampleRate: zod.z.number(),
66238
+ sampleRate: zod.z.number().meta(readingMeta("none")),
66207
66239
  duplex: zod.z.enum(["half", "full"]),
66208
66240
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
66209
- maxBacklogMs: zod.z.number()
66241
+ maxBacklogMs: zod.z.number().meta(readingMeta("duration-ms"))
66210
66242
  });
66211
66243
  var IntercomStatusSchema = zod.z.object({
66212
66244
  /** True while a talk session is open. */
@@ -66433,13 +66465,13 @@ var LawnMowerControlStatusSchema = zod.z.object({
66433
66465
  activity: LawnMowerActivitySchema,
66434
66466
  /** 0..100 battery percentage. Null when the device has no battery
66435
66467
  * reading. */
66436
- batteryLevel: zod.z.number().min(0).max(100).nullable(),
66468
+ batteryLevel: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66437
66469
  /** 0..100 mowing-completion percentage of the current task, or null when no
66438
66470
  * task is active / progress is unavailable. */
66439
- progressPercent: zod.z.number().min(0).max(100).nullable(),
66471
+ progressPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66440
66472
  /** Current device/event code (dynamic — mostly status, sometimes an error),
66441
66473
  * or null when unknown. */
66442
- currentCode: zod.z.number().nullable(),
66474
+ currentCode: zod.z.number().nullable().meta(readingMeta("none")),
66443
66475
  /** Human label for {@link currentCode}, or null when undecodable. */
66444
66476
  currentCodeLabel: zod.z.string().nullable(),
66445
66477
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -67163,7 +67195,7 @@ var MediaPlayerStatusSchema = zod.z.object({
67163
67195
  state: MediaPlayerStateSchema,
67164
67196
  /** Volume as 0..100 inclusive. Null when the device has no volume
67165
67197
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
67166
- volumeLevel: zod.z.number().min(0).max(100).nullable(),
67198
+ volumeLevel: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67167
67199
  /** Mute toggle distinct from volume=0. Null when no mute surface.
67168
67200
  * Pair with `DeviceFeature.MediaPlayerMute`. */
67169
67201
  isMuted: zod.z.boolean().nullable(),
@@ -67176,10 +67208,10 @@ var MediaPlayerStatusSchema = zod.z.object({
67176
67208
  currentMedia: MediaInfoSchema.nullable(),
67177
67209
  /** Current playback position in ms. Null when not seekable or
67178
67210
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
67179
- positionMs: zod.z.number().int().nonnegative().nullable(),
67211
+ positionMs: zod.z.number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
67180
67212
  /** Total duration of the current media in ms. Null when unknown
67181
67213
  * (live stream) or nothing is playing. */
67182
- durationMs: zod.z.number().int().nonnegative().nullable(),
67214
+ durationMs: zod.z.number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
67183
67215
  /** Shuffle toggle. Null when no shuffle surface.
67184
67216
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
67185
67217
  shuffle: zod.z.boolean().nullable(),
@@ -67549,7 +67581,7 @@ var motionCustomize = {
67549
67581
  eventName: "onMotionChanged"
67550
67582
  },
67551
67583
  deviceTypes: [DeviceType.Camera],
67552
- apply: "replace",
67584
+ apply: "add",
67553
67585
  applyAuthority: {
67554
67586
  kind: "camera-sensors",
67555
67587
  timeoutField: "motionCooldownMs"
@@ -67646,7 +67678,7 @@ var MotionStatusSchema = zod.z.object({
67646
67678
  * it, every provider seeded it at boot, and every session left it
67647
67679
  * `detected: true` after its detach.
67648
67680
  */
67649
- autoClearAfterMs: zod.z.number().nullable()
67681
+ autoClearAfterMs: zod.z.number().nullable().meta(readingMeta("duration-ms"))
67650
67682
  });
67651
67683
  /**
67652
67684
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -67828,7 +67860,7 @@ var MotionZoneRegionSchema = zod.z.object({
67828
67860
  * the grid region(s). */
67829
67861
  var MotionZoneStatusSchema = zod.z.object({
67830
67862
  enabled: zod.z.boolean(),
67831
- sensitivity: zod.z.number(),
67863
+ sensitivity: zod.z.number().meta(readingMeta("none")),
67832
67864
  /** Grid region(s). Today exactly one `grid` shape. */
67833
67865
  regions: zod.z.array(MotionZoneRegionSchema),
67834
67866
  lastFetchedAt: zod.z.number()
@@ -68200,7 +68232,7 @@ var NavigationStatusSchema = zod.z.object({
68200
68232
  following: zod.z.boolean(),
68201
68233
  flash: zod.z.boolean(),
68202
68234
  lightMode: NavigationLightModeSchema,
68203
- lightLevel: zod.z.number().min(40).max(100),
68235
+ lightLevel: zod.z.number().min(40).max(100).meta(readingMeta("none")),
68204
68236
  /** Ms epoch when the slice was last updated. */
68205
68237
  lastChangedAt: zod.z.number()
68206
68238
  });
@@ -68334,13 +68366,13 @@ var NetworkLinkStatusSchema = zod.z.object({
68334
68366
  * one whose reading has not landed must not be drawn at 0 %. Consumers
68335
68367
  * SKIP a null rather than coerce it.
68336
68368
  */
68337
- signalPercent: zod.z.number().min(0).max(100).nullable(),
68369
+ signalPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68338
68370
  /** Raw received signal strength in dBm, when the firmware reports one. */
68339
- rssiDbm: zod.z.number().optional(),
68371
+ rssiDbm: zod.z.number().optional().meta(readingMeta("signal-level")),
68340
68372
  /** Network name of a wireless link, when the firmware reports it. */
68341
68373
  ssid: zod.z.string().optional(),
68342
68374
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
68343
- lastUpdated: zod.z.number()
68375
+ lastUpdated: zod.z.number().meta(readingMeta("none"))
68344
68376
  });
68345
68377
  var networkLinkCapability = {
68346
68378
  name: "network-link",
@@ -68790,7 +68822,7 @@ var NotifierStatusSchema = zod.z.object({
68790
68822
  lastError: zod.z.string().nullable(),
68791
68823
  /** Number of deliveries currently buffered server-side (rate-limited
68792
68824
  * notifiers may queue). 0 when the provider sends synchronously. */
68793
- queueDepth: zod.z.number().int().nonnegative(),
68825
+ queueDepth: zod.z.number().int().nonnegative().meta(readingMeta("count")),
68794
68826
  /** Per-feature capability matrix — authoritative source for the
68795
68827
  * compose-form field gating. */
68796
68828
  supports: NotifierSupportsSchema
@@ -68882,11 +68914,9 @@ onSent: { data: zod.z.object({
68882
68914
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
68883
68915
  * for: water flow, distance, weight, frequency, signal strength, …).
68884
68916
  *
68885
- * The `unit` and `precision` fields carry the live display metadata that
68886
- * the upstream source provides with each state push (HA
68887
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
68888
- * The UI reads them directly from the slice — they are the single source
68889
- * 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).
68890
68920
  *
68891
68921
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
68892
68922
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -68895,15 +68925,8 @@ onSent: { data: zod.z.object({
68895
68925
  * numeric values land here.
68896
68926
  */
68897
68927
  var NumericSensorStatusSchema = zod.z.object({
68898
- value: zod.z.number(),
68899
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
68900
- * Populated live from the upstream source on each state push.
68901
- * Absent for unitless sensors. */
68902
- unit: zod.z.string().optional(),
68903
- /** Suggested decimal places for numeric display.
68904
- * Populated live from the upstream source when provided.
68905
- * Falls back to auto-formatting when absent. */
68906
- 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")),
68907
68930
  /** Ms epoch when the slice was last updated. */
68908
68931
  lastFetchedAt: zod.z.number()
68909
68932
  });
@@ -68927,11 +68950,11 @@ var numericSensorCapability = {
68927
68950
  */
68928
68951
  durability: "restored",
68929
68952
  /**
68930
- * 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.
68931
68955
  */
68932
68956
  history: { fields: { value: {
68933
68957
  kind: "gauge",
68934
- unitFrom: "unit",
68935
68958
  minIntervalMs: 3e4
68936
68959
  } } },
68937
68960
  /** Clock fields: written, but excluded from the compare that decides
@@ -69332,23 +69355,26 @@ var PetFeederStatusSchema = zod.z.object({
69332
69355
  /** Food currently in the bowl (grams). Null when the device has not
69333
69356
  * reported a reading yet. On dual-hopper models this is the combined
69334
69357
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
69335
- foodLevel: zod.z.number().nullable(),
69358
+ foodLevel: zod.z.number().nullable().meta(readingMeta("none")),
69336
69359
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
69337
69360
  * single-hopper models. */
69338
- food1: zod.z.number().nullable(),
69361
+ food1: zod.z.number().nullable().meta(readingMeta("none")),
69339
69362
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
69340
69363
  * single-hopper models. */
69341
- food2: zod.z.number().nullable(),
69364
+ food2: zod.z.number().nullable().meta(readingMeta("none")),
69342
69365
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
69343
69366
  * (`device_class: problem`, on = low). True when the bowl is empty /
69344
69367
  * below the feeder's low threshold. */
69345
69368
  lowFood: zod.z.boolean(),
69346
69369
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
69347
69370
  * device has no battery reading. */
69348
- batteryPower: zod.z.number().min(0).max(100).nullable(),
69349
- /** Days of desiccant life remaining. Null when the model has no
69350
- * desiccant sensor. */
69351
- 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")),
69352
69378
  /** True while a feed is in progress. */
69353
69379
  feeding: zod.z.boolean(),
69354
69380
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -69359,7 +69385,7 @@ var PetFeederStatusSchema = zod.z.object({
69359
69385
  * with `errorCode` for consumers that want the raw integer. */
69360
69386
  error: zod.z.string().nullable(),
69361
69387
  /** Raw device error code (0 / null = no error). */
69362
- errorCode: zod.z.number().nullable(),
69388
+ errorCode: zod.z.number().nullable().meta(readingMeta("none")),
69363
69389
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
69364
69390
  isDualHopper: zod.z.boolean(),
69365
69391
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -69369,7 +69395,7 @@ var PetFeederStatusSchema = zod.z.object({
69369
69395
  /** Play a chime when dispensing. */
69370
69396
  feedSound: zod.z.boolean(),
69371
69397
  /** Speaker / prompt volume level (device-scaled integer). */
69372
- volume: zod.z.number(),
69398
+ volume: zod.z.number().meta(readingMeta("none")),
69373
69399
  /** Ms epoch when the slice was last refreshed from the cloud. */
69374
69400
  lastFetchedAt: zod.z.number()
69375
69401
  });
@@ -69488,7 +69514,7 @@ var petFeederCapability = {
69488
69514
  },
69489
69515
  desiccantLeftDays: {
69490
69516
  kind: "gauge",
69491
- unit: "days"
69517
+ unit: "s"
69492
69518
  }
69493
69519
  } }
69494
69520
  };
@@ -69949,36 +69975,19 @@ var platformProbeCapability = {
69949
69975
  */
69950
69976
  var PowerMeterStatusSchema = zod.z.object({
69951
69977
  /** Instantaneous power draw in watts. */
69952
- watts: zod.z.number().optional().meta({
69953
- unit: "W",
69954
- label: "Power"
69955
- }),
69978
+ watts: zod.z.number().optional().meta(readingMeta("power", { label: "Power" })),
69956
69979
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
69957
- kwhTotal: zod.z.number().optional().meta({
69958
- unit: "kWh",
69959
- label: "Energy"
69960
- }),
69980
+ kwhTotal: zod.z.number().optional().meta(readingMeta("energy", { label: "Energy" })),
69961
69981
  /** Voltage in volts. */
69962
- volts: zod.z.number().optional(),
69982
+ volts: zod.z.number().optional().meta(readingMeta("voltage")),
69963
69983
  /** Current in amperes. */
69964
- amps: zod.z.number().optional(),
69965
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
69966
- 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")),
69967
69987
  /** Power factor (0..1) if reported. */
69968
- powerFactor: zod.z.number().min(0).max(1).optional(),
69988
+ powerFactor: zod.z.number().min(0).max(1).optional().meta(readingMeta("ratio")),
69969
69989
  /** Ms epoch when the slice was last updated. */
69970
- lastFetchedAt: zod.z.number(),
69971
- /** Live display unit of the single metric this slice carries (e.g. HA
69972
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
69973
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
69974
- * unit per slice is unambiguous. The UI prefers this over the role's
69975
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
69976
- unit: zod.z.string().optional(),
69977
- /** Suggested decimal places for numeric display.
69978
- * Populated live from the upstream source when provided (e.g. HA
69979
- * `attributes.suggested_display_precision`). Falls back to
69980
- * auto-formatting when absent. */
69981
- precision: zod.z.number().int().min(0).max(10).optional()
69990
+ lastFetchedAt: zod.z.number()
69982
69991
  });
69983
69992
  var powerMeterCapability = {
69984
69993
  name: "power-meter",
@@ -70042,12 +70051,7 @@ var powerMeterCapability = {
70042
70051
  } },
70043
70052
  /** Clock fields: written, but excluded from the compare that decides
70044
70053
  * whether persisting is worth a SQLite commit. */
70045
- volatileStateFields: ["lastFetchedAt"],
70046
- customize: {
70047
- version: 1,
70048
- advanced: "derive",
70049
- targets: []
70050
- }
70054
+ volatileStateFields: ["lastFetchedAt"]
70051
70055
  };
70052
70056
  /**
70053
70057
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -70067,11 +70071,11 @@ var powerMeterCapability = {
70067
70071
  */
70068
70072
  var GpsLocationSchema = zod.z.object({
70069
70073
  /** Latitude in decimal degrees, -90..90. */
70070
- latitude: zod.z.number().min(-90).max(90),
70074
+ latitude: zod.z.number().min(-90).max(90).meta(readingMeta("angle")),
70071
70075
  /** Longitude in decimal degrees, -180..180. */
70072
- longitude: zod.z.number().min(-180).max(180),
70076
+ longitude: zod.z.number().min(-180).max(180).meta(readingMeta("angle")),
70073
70077
  /** Reported accuracy in meters (lower = better). */
70074
- accuracyMeters: zod.z.number().nonnegative()
70078
+ accuracyMeters: zod.z.number().nonnegative().meta(readingMeta("length"))
70075
70079
  });
70076
70080
  var PresenceStatusSchema = zod.z.object({
70077
70081
  /** `home` / `not_home` / any user-defined zone name. */
@@ -70083,7 +70087,7 @@ var PresenceStatusSchema = zod.z.object({
70083
70087
  gps: GpsLocationSchema.nullable(),
70084
70088
  /** Optional battery level of the tracking device (0..100). Null
70085
70089
  * when not reported. */
70086
- batteryPercent: zod.z.number().min(0).max(100).nullable(),
70090
+ batteryPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
70087
70091
  /** Ms epoch when the slice was last updated. */
70088
70092
  lastChangedAt: zod.z.number()
70089
70093
  });
@@ -70133,19 +70137,10 @@ var presenceCapability = {
70133
70137
  * `sourceInfo`.
70134
70138
  */
70135
70139
  var PressureSensorStatusSchema = zod.z.object({
70136
- /** Current pressure in hPa. */
70137
- 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 })),
70138
70142
  /** Ms epoch when the slice was last updated. */
70139
- lastFetchedAt: zod.z.number(),
70140
- /** Live display unit from the upstream source (e.g. HA
70141
- * `attributes.unit_of_measurement`). The UI prefers this over the
70142
- * role's canonical unit. Absent → fall back to the canonical unit. */
70143
- unit: zod.z.string().optional(),
70144
- /** Suggested decimal places for numeric display.
70145
- * Populated live from the upstream source when provided (e.g. HA
70146
- * `attributes.suggested_display_precision`). Falls back to
70147
- * auto-formatting when absent. */
70148
- precision: zod.z.number().int().min(0).max(10).optional()
70143
+ lastFetchedAt: zod.z.number()
70149
70144
  });
70150
70145
  var pressureSensorCapability = {
70151
70146
  name: "pressure-sensor",
@@ -70686,8 +70681,8 @@ var ptzCapability = {
70686
70681
  var AutotrackTargetTypeSchema = zod.z.string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
70687
70682
  var PtzAutotrackSettingsSchema = zod.z.object({
70688
70683
  targetType: AutotrackTargetTypeSchema,
70689
- stopDelaySeconds: zod.z.number().int().min(0).max(300),
70690
- 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"))
70691
70686
  });
70692
70687
  /**
70693
70688
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -70841,7 +70836,7 @@ var RunningStateStatusSchema = zod.z.object({
70841
70836
  /** null = unknown — never coerced to false (D393). */
70842
70837
  running: zod.z.boolean().nullable(),
70843
70838
  /** Epoch ms the current boolean began; null while `running` is null. */
70844
- since: zod.z.number().nullable(),
70839
+ since: zod.z.number().nullable().meta(readingMeta("none")),
70845
70840
  source: RunningStateSourceSchema.meta({ label: "Source" }),
70846
70841
  phase: zod.z.string().nullable(),
70847
70842
  /** null until a reading has been published (D393). */
@@ -70856,7 +70851,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
70856
70851
  /** Generic Customize target for the running boolean (D936). */
70857
70852
  var runningCustomize = {
70858
70853
  version: 1,
70859
- advanced: "derive",
70860
70854
  targets: [{
70861
70855
  id: "running",
70862
70856
  cardinality: "scalar",
@@ -70876,7 +70870,7 @@ var runningCustomize = {
70876
70870
  DeviceType.Thermostat,
70877
70871
  DeviceType.PetFeeder
70878
70872
  ],
70879
- apply: "add-or-replace",
70873
+ apply: "add",
70880
70874
  path: "running",
70881
70875
  sources: [
70882
70876
  {
@@ -70892,6 +70886,7 @@ var runningCustomize = {
70892
70886
  requiredUnit: "W",
70893
70887
  transforms: [{
70894
70888
  kind: "threshold",
70889
+ comparisons: ["gte", "lte"],
70895
70890
  comparison: "gte",
70896
70891
  defaultThreshold: 5,
70897
70892
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -72514,9 +72509,9 @@ var RecordTriggerSchema = zod.z.enum([
72514
72509
  */
72515
72510
  var RecordWindowSchema = zod.z.object({
72516
72511
  trigger: RecordTriggerSchema,
72517
- day: zod.z.number().int().min(0).max(6),
72518
- startMinute: zod.z.number().int().min(0).max(1439),
72519
- 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"))
72520
72515
  });
72521
72516
  /** Status of one physical volume, as the camera itself describes it. */
72522
72517
  var OnboardStorageVolumeSchema = zod.z.object({
@@ -72615,10 +72610,10 @@ var RecordingOnboardStatusSchema = zod.z.object({
72615
72610
  /** Master "record to the card at all" switch. */
72616
72611
  enabled: zod.z.boolean().nullable(),
72617
72612
  overwriteWhenFull: zod.z.boolean().nullable(),
72618
- preRecordSec: zod.z.number().nullable(),
72619
- postRecordSec: zod.z.number().nullable(),
72613
+ preRecordSec: zod.z.number().nullable().meta(readingMeta("duration")),
72614
+ postRecordSec: zod.z.number().nullable().meta(readingMeta("duration")),
72620
72615
  /** Length of one recorded file, in minutes. */
72621
- segmentMinutes: zod.z.number().nullable(),
72616
+ segmentMinutes: zod.z.number().nullable().meta(readingMeta("none")),
72622
72617
  /** The primary track's weekly windows, flattened. */
72623
72618
  windows: zod.z.array(RecordWindowSchema),
72624
72619
  /**
@@ -72634,7 +72629,7 @@ var RecordingOnboardStatusSchema = zod.z.object({
72634
72629
  * provider reporting a non-zero count also reports the schedule as not
72635
72630
  * writable.
72636
72631
  */
72637
- unreadableWindows: zod.z.number(),
72632
+ unreadableWindows: zod.z.number().meta(readingMeta("count")),
72638
72633
  /**
72639
72634
  * The camera is scheduled to record and has NO usable storage.
72640
72635
  *
@@ -73500,7 +73495,7 @@ var CamStreamDescriptorSchema = zod.z.object({
73500
73495
  url: zod.z.string().optional(),
73501
73496
  codec: zod.z.string().optional(),
73502
73497
  resolution: CamStreamResolutionSchema.optional(),
73503
- fps: zod.z.number().positive().optional(),
73498
+ fps: zod.z.number().positive().optional().meta(readingMeta("rate")),
73504
73499
  label: zod.z.string().optional(),
73505
73500
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
73506
73501
  deviceFeatures: zod.z.array(zod.z.string()).optional(),
@@ -73587,18 +73582,18 @@ var StreamProfileSchema = zod.z.enum([
73587
73582
  ]);
73588
73583
  /** The current encoding config of one stream profile. */
73589
73584
  var StreamProfileConfigSchema = zod.z.object({
73590
- width: zod.z.number(),
73591
- height: zod.z.number(),
73585
+ width: zod.z.number().meta(readingMeta("none")),
73586
+ height: zod.z.number().meta(readingMeta("none")),
73592
73587
  codec: zod.z.enum(["h264", "h265"]),
73593
- framerate: zod.z.number(),
73594
- bitrate: zod.z.number(),
73588
+ framerate: zod.z.number().meta(readingMeta("rate")),
73589
+ bitrate: zod.z.number().meta(readingMeta("bitrate")),
73595
73590
  bitrateMode: zod.z.enum(["vbr", "cbr"]).optional(),
73596
73591
  encoderProfile: zod.z.enum([
73597
73592
  "high",
73598
73593
  "main",
73599
73594
  "baseline"
73600
73595
  ]).optional(),
73601
- gop: zod.z.number().optional(),
73596
+ gop: zod.z.number().optional().meta(readingMeta("none")),
73602
73597
  /**
73603
73598
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
73604
73599
  *
@@ -74076,12 +74071,7 @@ var switchCapability = {
74076
74071
  label: "On",
74077
74072
  kind: "boolean"
74078
74073
  }
74079
- }] },
74080
- customize: {
74081
- version: 1,
74082
- advanced: "derive",
74083
- targets: []
74084
- }
74074
+ }] }
74085
74075
  };
74086
74076
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
74087
74077
  function hasControlCharacter(value) {
@@ -74791,35 +74781,21 @@ var tamperCapability = {
74791
74781
  * entries with `device_class: temperature` and any future native
74792
74782
  * thermometer.
74793
74783
  *
74794
- * Unit normalisation: providers convert to Celsius before storing.
74795
- * The slice value is always Celsius so cross-cap aggregators
74796
- * (climate-control's `currentTemp`, energy analytics) can compose
74797
- * without per-source unit fixups. The canonical display unit (`°C`) is
74798
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
74799
- * `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.
74800
74786
  *
74801
74787
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
74802
74788
  * frozen feed (provider hung) distinct from a "temperature hasn't
74803
74789
  * changed" steady state.
74804
74790
  */
74805
74791
  var TemperatureSensorStatusSchema = zod.z.object({
74806
- /** Current temperature in Celsius. */
74807
- celsius: zod.z.number().meta({
74808
- 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,
74809
74795
  label: "Temperature"
74810
- }),
74796
+ })),
74811
74797
  /** Ms epoch when the slice was last updated (push or poll). */
74812
- lastFetchedAt: zod.z.number(),
74813
- /** Live display unit from the upstream source (e.g. HA
74814
- * `attributes.unit_of_measurement`). The UI prefers this over the
74815
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
74816
- * Absent → the UI falls back to the role's canonical unit. */
74817
- unit: zod.z.string().optional(),
74818
- /** Suggested decimal places for numeric display.
74819
- * Populated live from the upstream source when provided (e.g. HA
74820
- * `attributes.suggested_display_precision`). Falls back to
74821
- * auto-formatting when absent. */
74822
- precision: zod.z.number().int().min(0).max(10).optional()
74798
+ lastFetchedAt: zod.z.number()
74823
74799
  });
74824
74800
  var temperatureSensorCapability = {
74825
74801
  name: "temperature-sensor",
@@ -74850,12 +74826,7 @@ var temperatureSensorCapability = {
74850
74826
  } } },
74851
74827
  /** Clock fields: written, but excluded from the compare that decides
74852
74828
  * whether persisting is worth a SQLite commit. */
74853
- volatileStateFields: ["lastFetchedAt"],
74854
- customize: {
74855
- version: 1,
74856
- advanced: "derive",
74857
- targets: []
74858
- }
74829
+ volatileStateFields: ["lastFetchedAt"]
74859
74830
  };
74860
74831
  /**
74861
74832
  * toast — system-scoped singleton capability that streams toast
@@ -75201,16 +75172,16 @@ var VacuumControlStatusSchema = zod.z.object({
75201
75172
  state: VacuumStateSchema,
75202
75173
  /** 0..100 battery percentage. Null when the device has no battery
75203
75174
  * reading. */
75204
- batteryLevel: zod.z.number().min(0).max(100).nullable(),
75175
+ batteryLevel: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
75205
75176
  /** Current fan-speed token (provider-verbatim). Null when unknown or
75206
75177
  * the vacuum has no speed control. */
75207
75178
  fanSpeed: zod.z.string().nullable(),
75208
75179
  /** Speed tokens the hardware accepts — drives the UI selector. */
75209
75180
  availableFanSpeeds: zod.z.array(zod.z.string()),
75210
75181
  /** 0..100 cleaning-completion percentage of the current task, or null. */
75211
- progressPercent: zod.z.number().min(0).max(100).nullable(),
75182
+ progressPercent: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
75212
75183
  /** Current error code (0 / null = no error). */
75213
- errorCode: zod.z.number().nullable(),
75184
+ errorCode: zod.z.number().nullable().meta(readingMeta("none")),
75214
75185
  /** Human label for {@link errorCode}, or null when none / undecodable. */
75215
75186
  errorLabel: zod.z.string().nullable(),
75216
75187
  /** Ms epoch when the slice was last updated. */
@@ -75306,7 +75277,7 @@ var ValveStatusSchema = zod.z.object({
75306
75277
  state: ValveStateSchema,
75307
75278
  /** 0 = fully closed, 100 = fully open. Null when the device has no
75308
75279
  * intermediate position surface. */
75309
- position: zod.z.number().min(0).max(100).nullable(),
75280
+ position: zod.z.number().min(0).max(100).nullable().meta(readingMeta("percentage")),
75310
75281
  /** Ms epoch when the slice was last updated. */
75311
75282
  lastChangedAt: zod.z.number()
75312
75283
  });
@@ -75414,9 +75385,9 @@ var vibrationCapability = {
75414
75385
  */
75415
75386
  var WaterHeaterStatusSchema = zod.z.object({
75416
75387
  /** Current measured temperature. Null when not reported. */
75417
- currentTemp: zod.z.number().nullable(),
75388
+ currentTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75418
75389
  /** Target temperature setpoint. Null when no setpoint surface. */
75419
- targetTemp: zod.z.number().nullable(),
75390
+ targetTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75420
75391
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
75421
75392
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
75422
75393
  * device reports an unknown state. */
@@ -75427,9 +75398,9 @@ var WaterHeaterStatusSchema = zod.z.object({
75427
75398
  * has no away surface. */
75428
75399
  away: zod.z.boolean().nullable(),
75429
75400
  /** HA `min_temp` attribute. Null when not reported. */
75430
- minTemp: zod.z.number().nullable(),
75401
+ minTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75431
75402
  /** HA `max_temp` attribute. Null when not reported. */
75432
- maxTemp: zod.z.number().nullable(),
75403
+ maxTemp: zod.z.number().nullable().meta(readingMeta("temperature")),
75433
75404
  /** Ms epoch when the slice was last updated. */
75434
75405
  lastChangedAt: zod.z.number()
75435
75406
  });
@@ -75514,25 +75485,18 @@ var waterHeaterCapability = {
75514
75485
  * Forecast deferred — current conditions only for v1.
75515
75486
  */
75516
75487
  var WeatherStatusSchema = zod.z.object({
75517
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
75518
- * when no condition has been reported yet. */
75488
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
75519
75489
  condition: zod.z.string().nullable(),
75520
- /** Current temperature in the reported unit. Null when not provided. */
75521
- temperature: zod.z.number().nullable(),
75522
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
75523
- 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 })),
75524
75492
  /** Relative humidity (0..100). Null when not provided. */
75525
- humidity: zod.z.number().min(0).max(100).nullable(),
75526
- /** Barometric pressure in the reported unit. Null when not provided. */
75527
- pressure: zod.z.number().nullable(),
75528
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
75529
- pressureUnit: zod.z.string().nullable(),
75530
- /** Wind speed in the reported unit. Null when not provided. */
75531
- windSpeed: zod.z.number().nullable(),
75532
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
75533
- 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")),
75534
75498
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
75535
- windBearing: zod.z.number().nullable(),
75499
+ windBearing: zod.z.number().nullable().meta(readingMeta("angle")),
75536
75500
  /** Ms epoch when the slice was last updated. */
75537
75501
  lastFetchedAt: zod.z.number()
75538
75502
  });
@@ -75560,12 +75524,12 @@ var weatherCapability = {
75560
75524
  */
75561
75525
  durability: "session",
75562
75526
  /**
75563
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
75527
+ * History (D850): recorded series. Numbers are the quantity base (D944).
75564
75528
  */
75565
75529
  history: { fields: {
75566
75530
  temperature: {
75567
75531
  kind: "gauge",
75568
- unitFrom: "temperatureUnit",
75532
+ unit: "°C",
75569
75533
  deadband: .2
75570
75534
  },
75571
75535
  humidity: {
@@ -75575,12 +75539,12 @@ var weatherCapability = {
75575
75539
  },
75576
75540
  pressure: {
75577
75541
  kind: "gauge",
75578
- unitFrom: "pressureUnit",
75542
+ unit: "hPa",
75579
75543
  deadband: .5
75580
75544
  },
75581
75545
  windSpeed: {
75582
75546
  kind: "gauge",
75583
- unitFrom: "windSpeedUnit",
75547
+ unit: "m/s",
75584
75548
  deadband: .5
75585
75549
  }
75586
75550
  } }
@@ -76791,6 +76755,11 @@ var UNIT_TABLE = {
76791
76755
  factor: 1609.344,
76792
76756
  offset: 0
76793
76757
  },
76758
+ "m²": {
76759
+ dimension: "area",
76760
+ factor: 1,
76761
+ offset: 0
76762
+ },
76794
76763
  lx: {
76795
76764
  dimension: "illuminance",
76796
76765
  factor: 1,
@@ -76895,6 +76864,121 @@ var UNIT_TABLE = {
76895
76864
  dimension: "percentage",
76896
76865
  factor: 1,
76897
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
76898
76982
  }
76899
76983
  };
76900
76984
  /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
@@ -91517,7 +91601,6 @@ var SENSOR_CAP_VOCABULARY = {
91517
91601
  field: "value",
91518
91602
  quantity: t("the reading", "il valore"),
91519
91603
  label: t("Reading", "Valore"),
91520
- unitField: "unit",
91521
91604
  icon: "gauge"
91522
91605
  }]
91523
91606
  },
@@ -91653,7 +91736,7 @@ var SENSOR_CAP_VOCABULARY = {
91653
91736
  field: "temperature",
91654
91737
  quantity: t("the outdoor temperature", "la temperatura esterna"),
91655
91738
  label: t("Temperature", "Temperatura"),
91656
- unitField: "temperatureUnit",
91739
+ unit: "°C",
91657
91740
  icon: "thermometer"
91658
91741
  },
91659
91742
  {
@@ -91669,7 +91752,7 @@ var SENSOR_CAP_VOCABULARY = {
91669
91752
  field: "windSpeed",
91670
91753
  quantity: t("the wind speed", "la velocità del vento"),
91671
91754
  label: t("Wind", "Vento"),
91672
- unitField: "windSpeedUnit",
91755
+ unit: "m/s",
91673
91756
  icon: "wind"
91674
91757
  },
91675
91758
  {
@@ -91677,7 +91760,7 @@ var SENSOR_CAP_VOCABULARY = {
91677
91760
  field: "pressure",
91678
91761
  quantity: t("the air pressure", "la pressione atmosferica"),
91679
91762
  label: t("Pressure", "Pressione"),
91680
- unitField: "pressureUnit",
91763
+ unit: "hPa",
91681
91764
  icon: "gauge"
91682
91765
  }
91683
91766
  ]
@@ -95345,7 +95428,7 @@ var SESSION_ONLY_POLICY = {
95345
95428
  };
95346
95429
  /**
95347
95430
  * Cap history catalog, projected from every `*.cap.ts` `history` block.
95348
- * 41 caps with series (74 series), 30 caps answered `'none'`.
95431
+ * 41 caps with series (75 series), 30 caps answered `'none'`.
95349
95432
  * Field keys are `runtimeState` paths (`a.b` into fixed objects only).
95350
95433
  */
95351
95434
  var HISTORY_CATALOG = {
@@ -95362,12 +95445,12 @@ var HISTORY_CATALOG = {
95362
95445
  },
95363
95446
  "pm25": {
95364
95447
  kind: "gauge",
95365
- unit: "ug/m3",
95448
+ unit: "µg/m³",
95366
95449
  deadband: 1
95367
95450
  },
95368
95451
  "pm10": {
95369
95452
  kind: "gauge",
95370
- unit: "ug/m3",
95453
+ unit: "µg/m³",
95371
95454
  deadband: 1
95372
95455
  },
95373
95456
  "aqi": {
@@ -95397,10 +95480,16 @@ var HISTORY_CATALOG = {
95397
95480
  deadbandPct: 10,
95398
95481
  minIntervalMs: 6e4
95399
95482
  } },
95400
- "battery": { "percentage": {
95401
- kind: "gauge",
95402
- unit: "%"
95403
- } },
95483
+ "battery": {
95484
+ "percentage": {
95485
+ kind: "gauge",
95486
+ unit: "%"
95487
+ },
95488
+ "volts": {
95489
+ kind: "gauge",
95490
+ unit: "V"
95491
+ }
95492
+ },
95404
95493
  "binary": { "on": { kind: "boolean" } },
95405
95494
  "brightness": { "percentage": {
95406
95495
  kind: "gauge",
@@ -95484,7 +95573,7 @@ var HISTORY_CATALOG = {
95484
95573
  },
95485
95574
  "enum-sensor": { "value": {
95486
95575
  kind: "enum",
95487
- skipWhenSet: "format"
95576
+ skipWhenSet: "timestamp"
95488
95577
  } },
95489
95578
  "fan-control": {
95490
95579
  "percentage": {
@@ -95557,7 +95646,6 @@ var HISTORY_CATALOG = {
95557
95646
  },
95558
95647
  "numeric-sensor": { "value": {
95559
95648
  kind: "gauge",
95560
- unitFrom: "unit",
95561
95649
  minIntervalMs: 3e4
95562
95650
  } },
95563
95651
  "pet-feeder": {
@@ -95571,7 +95659,7 @@ var HISTORY_CATALOG = {
95571
95659
  },
95572
95660
  "desiccantLeftDays": {
95573
95661
  kind: "gauge",
95574
- unit: "days"
95662
+ unit: "s"
95575
95663
  }
95576
95664
  },
95577
95665
  "power-meter": {
@@ -95675,7 +95763,7 @@ var HISTORY_CATALOG = {
95675
95763
  "weather": {
95676
95764
  "temperature": {
95677
95765
  kind: "gauge",
95678
- unitFrom: "temperatureUnit",
95766
+ unit: "°C",
95679
95767
  deadband: .2
95680
95768
  },
95681
95769
  "humidity": {
@@ -95685,12 +95773,12 @@ var HISTORY_CATALOG = {
95685
95773
  },
95686
95774
  "pressure": {
95687
95775
  kind: "gauge",
95688
- unitFrom: "pressureUnit",
95776
+ unit: "hPa",
95689
95777
  deadband: .5
95690
95778
  },
95691
95779
  "windSpeed": {
95692
95780
  kind: "gauge",
95693
- unitFrom: "windSpeedUnit",
95781
+ unit: "m/s",
95694
95782
  deadband: .5
95695
95783
  }
95696
95784
  }
@@ -95724,7 +95812,6 @@ var HISTORY_ITEM_CATALOG = { "consumables": {
95724
95812
  },
95725
95813
  "remaining.value": {
95726
95814
  kind: "gauge",
95727
- unitFrom: "remaining.unit",
95728
95815
  deadband: 1,
95729
95816
  displayName: "Remaining"
95730
95817
  }
@@ -95792,6 +95879,11 @@ var PulseOverrideDraftSchema = zod.z.object({
95792
95879
  targetId: zod.z.string().min(1),
95793
95880
  enabled: zod.z.boolean(),
95794
95881
  debounceMs: zod.z.number().nonnegative(),
95882
+ edges: zod.z.array(zod.z.enum([
95883
+ "rising",
95884
+ "button",
95885
+ "native-event"
95886
+ ])).min(1).optional(),
95795
95887
  sources: zod.z.array(OverrideSourceSchema).min(1)
95796
95888
  });
95797
95889
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: zod.z.enum([
@@ -98613,6 +98705,12 @@ Object.defineProperty(exports, "historyItemFieldPath", {
98613
98705
  return historyItemFieldPath;
98614
98706
  }
98615
98707
  });
98708
+ Object.defineProperty(exports, "historyUnitFor", {
98709
+ enumerable: true,
98710
+ get: function() {
98711
+ return historyUnitFor;
98712
+ }
98713
+ });
98616
98714
  Object.defineProperty(exports, "hydrateSchema", {
98617
98715
  enumerable: true,
98618
98716
  get: function() {
@@ -98697,6 +98795,12 @@ Object.defineProperty(exports, "isObjectInput", {
98697
98795
  return isObjectInput;
98698
98796
  }
98699
98797
  });
98798
+ Object.defineProperty(exports, "isReadingQuantity", {
98799
+ enumerable: true,
98800
+ get: function() {
98801
+ return isReadingQuantity;
98802
+ }
98803
+ });
98700
98804
  Object.defineProperty(exports, "isSecretClear", {
98701
98805
  enumerable: true,
98702
98806
  get: function() {