@camstack/types 1.2.446 → 1.2.447

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8060,6 +8060,791 @@ var ExpressionSourceSchema = zod.z.object({
8060
8060
  });
8061
8061
  });
8062
8062
  //#endregion
8063
+ //#region src/device/schema-fields.ts
8064
+ /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
8065
+ * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
8066
+ function unwrapSchema(schema) {
8067
+ let s = schema;
8068
+ while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
8069
+ return s;
8070
+ }
8071
+ function isMetaBag(value) {
8072
+ return typeof value === "object" && value !== null;
8073
+ }
8074
+ function metaQuantity(schema) {
8075
+ const bag = zod.z.globalRegistry.get(schema);
8076
+ if (isMetaBag(bag) && require_sleep.isReadingQuantity(bag.quantity)) return bag.quantity;
8077
+ }
8078
+ /** Quantity declared with `readingMeta` on the field or on the unwrapped leaf. */
8079
+ function quantityOfField(schema) {
8080
+ return metaQuantity(schema) ?? metaQuantity(unwrapSchema(schema));
8081
+ }
8082
+ function leafKind(schema) {
8083
+ const s = unwrapSchema(schema);
8084
+ if (s instanceof zod.z.ZodEnum) return {
8085
+ kind: "enum",
8086
+ enumValues: s.options
8087
+ };
8088
+ if (s instanceof zod.z.ZodNumber) return { kind: "number" };
8089
+ if (s instanceof zod.z.ZodString) return { kind: "string" };
8090
+ if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
8091
+ return null;
8092
+ }
8093
+ /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
8094
+ * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
8095
+ * so fields with null live values are still offered. Arrays / unknown shapes
8096
+ * are skipped — never throws. */
8097
+ function enumerateSchemaFields(schema, prefix = "") {
8098
+ const root = unwrapSchema(schema);
8099
+ if (!(root instanceof zod.z.ZodObject)) return [];
8100
+ const out = [];
8101
+ const shape = root.shape;
8102
+ for (const [key, field] of Object.entries(shape)) {
8103
+ const path = prefix ? `${prefix}.${key}` : key;
8104
+ const inner = unwrapSchema(field);
8105
+ if (inner instanceof zod.z.ZodObject) {
8106
+ out.push(...enumerateSchemaFields(inner, path));
8107
+ continue;
8108
+ }
8109
+ const leaf = leafKind(field);
8110
+ if (leaf) {
8111
+ const quantity = quantityOfField(field);
8112
+ out.push({
8113
+ path,
8114
+ kind: leaf.kind,
8115
+ ...leaf.enumValues ? { enumValues: leaf.enumValues } : {},
8116
+ ...quantity !== void 0 ? { quantity } : {}
8117
+ });
8118
+ }
8119
+ }
8120
+ return out;
8121
+ }
8122
+ /** Enumerate the per-item wireable fields of an item-array cap (see
8123
+ * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
8124
+ * `keyField` (the key comes from the link's `itemKey`, never from a wired
8125
+ * source), each tagged `item: true` so the authoring UI collects an
8126
+ * `itemKey` alongside the field. Never throws. */
8127
+ function enumerateItemArrayFields(itemArray) {
8128
+ return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
8129
+ ...f,
8130
+ item: true
8131
+ }));
8132
+ }
8133
+ //#endregion
8134
+ //#region src/device/source-info.ts
8135
+ /**
8136
+ * Identity envelope for a device's upstream-system metadata.
8137
+ *
8138
+ * Two jobs:
8139
+ *
8140
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
8141
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
8142
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
8143
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
8144
+ * without round-tripping through `stableId` / `id` mappings.
8145
+ *
8146
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
8147
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
8148
+ * for vendors where MAC is the natural key). For systems where the
8149
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
8150
+ * providers detect renames by matching `uniqueId` and patch `id`
8151
+ * in-place so dispatch keeps working.
8152
+ *
8153
+ * **Display unit and precision are NOT stored here.** They are a read-time
8154
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
8155
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
8156
+ * neither is repeated on each sample. The `device_class` tag is an
8157
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
8158
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
8159
+ *
8160
+ * The `raw` escape hatch carries provider-specific attributes that don't
8161
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
8162
+ * details, …). Providers control what lands here.
8163
+ */
8164
+ var SourceInfoSchema = zod.z.object({
8165
+ /** Live dispatch key — mutable when the source system allows rename. */
8166
+ id: zod.z.string(),
8167
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
8168
+ system: zod.z.string(),
8169
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
8170
+ * Used to detect rename when `id` changes. */
8171
+ uniqueId: zod.z.string().optional(),
8172
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
8173
+ quantity: require_sleep.ReadingQuantitySchema.optional(),
8174
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
8175
+ dateTimeShape: zod.z.enum([
8176
+ "date",
8177
+ "time",
8178
+ "datetime"
8179
+ ]).optional(),
8180
+ /** Provider-specific extras that don't fit the structured slots.
8181
+ * Whitelist what goes here per provider — do NOT dump entire HA
8182
+ * attribute blobs (would explode the meta JSON column). */
8183
+ raw: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
8184
+ });
8185
+ /**
8186
+ * Build the synthetic SourceInfo every existing provider falls back to
8187
+ * when no upstream value has been persisted. Keeps non-migrated providers
8188
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
8189
+ * changes — `id` reuses the CamStack stableId, `system` reuses the addon
8190
+ * id. Real upstream identity replaces this once a provider migrates and
8191
+ * calls `updateSourceInfo()` with concrete values.
8192
+ */
8193
+ function synthesizeSourceInfo(input) {
8194
+ return {
8195
+ id: input.stableId,
8196
+ system: input.addonId
8197
+ };
8198
+ }
8199
+ /**
8200
+ * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
8201
+ * patch values are ignored (no field clobber); to clear a field
8202
+ * explicitly, providers omit it from the patch and rebuild from scratch
8203
+ * via `updateSourceInfo({})` cycles — uncommon enough that we don't
8204
+ * bother with a `null`-means-delete sentinel.
8205
+ *
8206
+ * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
8207
+ * merges over existing `raw` keys instead of replacing the whole bag.
8208
+ */
8209
+ function mergeSourceInfo(prev, patch) {
8210
+ const next = { ...prev };
8211
+ for (const [k, v] of Object.entries(patch)) {
8212
+ if (v === void 0) continue;
8213
+ if (k === "raw" && typeof v === "object" && v !== null) {
8214
+ next.raw = {
8215
+ ...prev.raw,
8216
+ ...v
8217
+ };
8218
+ continue;
8219
+ }
8220
+ next[k] = v;
8221
+ }
8222
+ return next;
8223
+ }
8224
+ /**
8225
+ * Coerce common unit-of-measurement string variants to a canonical form
8226
+ * so the UI's display templates can rely on stable strings. Pure
8227
+ * passthrough for unknown units — kept conservative on purpose, the goal
8228
+ * is to normalise the long tail of HA `unit_of_measurement` values that
8229
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
8230
+ *
8231
+ * Extend the map as we encounter real-world data. Returning the raw
8232
+ * input for unknown values is intentional: providers should never
8233
+ * lose information silently here.
8234
+ */
8235
+ function normalizeUnit(raw) {
8236
+ if (raw == null) return void 0;
8237
+ const trimmed = raw.trim();
8238
+ if (trimmed === "") return void 0;
8239
+ const lower = trimmed.toLowerCase();
8240
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
8241
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
8242
+ if (lower === "k" || lower === "kelvin") return "K";
8243
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
8244
+ if (lower === "lx" || lower === "lux") return "lx";
8245
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
8246
+ if (lower === "kpa") return "kPa";
8247
+ if (lower === "pa") return "Pa";
8248
+ if (lower === "mbar" || lower === "millibar") return "mbar";
8249
+ if (lower === "w" || lower === "watt") return "W";
8250
+ if (lower === "kw" || lower === "kilowatt") return "kW";
8251
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
8252
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
8253
+ if (lower === "v" || lower === "volt") return "V";
8254
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
8255
+ if (lower === "ma" || lower === "milliampere") return "mA";
8256
+ if (lower === "va") return "VA";
8257
+ if (lower === "ppm") return "ppm";
8258
+ if (lower === "ppb") return "ppb";
8259
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
8260
+ if (lower === "km/h" || lower === "kph") return "km/h";
8261
+ if (lower === "mph") return "mph";
8262
+ if (lower === "m/s") return "m/s";
8263
+ if (lower === "m") return "m";
8264
+ if (lower === "m²" || lower === "m2") return "m²";
8265
+ if (lower === "mm") return "mm";
8266
+ if (lower === "cm") return "cm";
8267
+ if (lower === "km") return "km";
8268
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
8269
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
8270
+ if (lower === "kg") return "kg";
8271
+ if (lower === "g") return "g";
8272
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
8273
+ if (lower === "ml") return "mL";
8274
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
8275
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
8276
+ if (lower === "ms") return "ms";
8277
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
8278
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
8279
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
8280
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
8281
+ if (lower === "kib") return "KiB";
8282
+ if (lower === "mib") return "MiB";
8283
+ if (lower === "gib") return "GiB";
8284
+ if (lower === "tib") return "TiB";
8285
+ if (lower === "kb") return "kB";
8286
+ if (lower === "mb") return "MB";
8287
+ if (lower === "gb") return "GB";
8288
+ if (lower === "dbm") return "dBm";
8289
+ if (lower === "dbfs") return "dBFS";
8290
+ if (lower === "inhg") return "inHg";
8291
+ if (lower === "mm/h") return "mm/h";
8292
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
8293
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
8294
+ if (lower === "kbps") return "kbps";
8295
+ if (lower === "fps") return "fps";
8296
+ return trimmed;
8297
+ }
8298
+ /**
8299
+ * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
8300
+ * blob is a free-form `Record<string, unknown>` shared with hardware-
8301
+ * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
8302
+ * lives under a single nested key to keep it composable with those.
8303
+ */
8304
+ var SOURCE_INFO_METADATA_KEY = "sourceInfo";
8305
+ /**
8306
+ * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
8307
+ * with Zod. Returns `null` when the blob is missing or the nested
8308
+ * `sourceInfo` key fails validation — callers fall back to the
8309
+ * synthetic default in that case (see `synthesizeSourceInfo`).
8310
+ */
8311
+ function extractSourceInfoFromMetadata(metadata) {
8312
+ if (!metadata) return null;
8313
+ const raw = metadata[SOURCE_INFO_METADATA_KEY];
8314
+ if (!raw) return null;
8315
+ const parsed = SourceInfoSchema.safeParse(raw);
8316
+ if (!parsed.success) return null;
8317
+ return parsed.data;
8318
+ }
8319
+ //#endregion
8320
+ //#region src/units/unit-table.ts
8321
+ /**
8322
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
8323
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
8324
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
8325
+ * decimals). K derives from `°C = K − 273.15`.
8326
+ */
8327
+ var UNIT_TABLE = {
8328
+ "°C": {
8329
+ dimension: "temperature",
8330
+ factor: 1,
8331
+ offset: 0
8332
+ },
8333
+ "°F": {
8334
+ dimension: "temperature",
8335
+ factor: 5 / 9,
8336
+ offset: -160 / 9
8337
+ },
8338
+ K: {
8339
+ dimension: "temperature",
8340
+ factor: 1,
8341
+ offset: -273.15
8342
+ },
8343
+ hPa: {
8344
+ dimension: "pressure",
8345
+ factor: 1,
8346
+ offset: 0
8347
+ },
8348
+ kPa: {
8349
+ dimension: "pressure",
8350
+ factor: 10,
8351
+ offset: 0
8352
+ },
8353
+ Pa: {
8354
+ dimension: "pressure",
8355
+ factor: .01,
8356
+ offset: 0
8357
+ },
8358
+ mbar: {
8359
+ dimension: "pressure",
8360
+ factor: 1,
8361
+ offset: 0
8362
+ },
8363
+ bar: {
8364
+ dimension: "pressure",
8365
+ factor: 1e3,
8366
+ offset: 0
8367
+ },
8368
+ inHg: {
8369
+ dimension: "pressure",
8370
+ factor: 33.8639,
8371
+ offset: 0
8372
+ },
8373
+ mmHg: {
8374
+ dimension: "pressure",
8375
+ factor: 1.33322,
8376
+ offset: 0
8377
+ },
8378
+ psi: {
8379
+ dimension: "pressure",
8380
+ factor: 68.9476,
8381
+ offset: 0
8382
+ },
8383
+ "m/s": {
8384
+ dimension: "speed",
8385
+ factor: 1,
8386
+ offset: 0
8387
+ },
8388
+ "km/h": {
8389
+ dimension: "speed",
8390
+ factor: 1 / 3.6,
8391
+ offset: 0
8392
+ },
8393
+ mph: {
8394
+ dimension: "speed",
8395
+ factor: .44704,
8396
+ offset: 0
8397
+ },
8398
+ kn: {
8399
+ dimension: "speed",
8400
+ factor: .514444,
8401
+ offset: 0
8402
+ },
8403
+ "mm/h": {
8404
+ dimension: "precipitation-rate",
8405
+ factor: 1,
8406
+ offset: 0
8407
+ },
8408
+ "in/h": {
8409
+ dimension: "precipitation-rate",
8410
+ factor: 25.4,
8411
+ offset: 0
8412
+ },
8413
+ m: {
8414
+ dimension: "length",
8415
+ factor: 1,
8416
+ offset: 0
8417
+ },
8418
+ mm: {
8419
+ dimension: "length",
8420
+ factor: .001,
8421
+ offset: 0
8422
+ },
8423
+ cm: {
8424
+ dimension: "length",
8425
+ factor: .01,
8426
+ offset: 0
8427
+ },
8428
+ km: {
8429
+ dimension: "length",
8430
+ factor: 1e3,
8431
+ offset: 0
8432
+ },
8433
+ in: {
8434
+ dimension: "length",
8435
+ factor: .0254,
8436
+ offset: 0
8437
+ },
8438
+ ft: {
8439
+ dimension: "length",
8440
+ factor: .3048,
8441
+ offset: 0
8442
+ },
8443
+ mi: {
8444
+ dimension: "length",
8445
+ factor: 1609.344,
8446
+ offset: 0
8447
+ },
8448
+ "m²": {
8449
+ dimension: "area",
8450
+ factor: 1,
8451
+ offset: 0
8452
+ },
8453
+ lx: {
8454
+ dimension: "illuminance",
8455
+ factor: 1,
8456
+ offset: 0
8457
+ },
8458
+ "W/m²": {
8459
+ dimension: "irradiance",
8460
+ factor: 1,
8461
+ offset: 0
8462
+ },
8463
+ W: {
8464
+ dimension: "power",
8465
+ factor: 1,
8466
+ offset: 0
8467
+ },
8468
+ kW: {
8469
+ dimension: "power",
8470
+ factor: 1e3,
8471
+ offset: 0
8472
+ },
8473
+ VA: {
8474
+ dimension: "apparent-power",
8475
+ factor: 1,
8476
+ offset: 0
8477
+ },
8478
+ Wh: {
8479
+ dimension: "energy",
8480
+ factor: 1,
8481
+ offset: 0
8482
+ },
8483
+ kWh: {
8484
+ dimension: "energy",
8485
+ factor: 1e3,
8486
+ offset: 0
8487
+ },
8488
+ MWh: {
8489
+ dimension: "energy",
8490
+ factor: 1e6,
8491
+ offset: 0
8492
+ },
8493
+ V: {
8494
+ dimension: "voltage",
8495
+ factor: 1,
8496
+ offset: 0
8497
+ },
8498
+ mV: {
8499
+ dimension: "voltage",
8500
+ factor: .001,
8501
+ offset: 0
8502
+ },
8503
+ A: {
8504
+ dimension: "current",
8505
+ factor: 1,
8506
+ offset: 0
8507
+ },
8508
+ mA: {
8509
+ dimension: "current",
8510
+ factor: .001,
8511
+ offset: 0
8512
+ },
8513
+ ppm: {
8514
+ dimension: "concentration-volume",
8515
+ factor: 1,
8516
+ offset: 0
8517
+ },
8518
+ ppb: {
8519
+ dimension: "concentration-volume",
8520
+ factor: .001,
8521
+ offset: 0
8522
+ },
8523
+ "µg/m³": {
8524
+ dimension: "concentration-mass",
8525
+ factor: 1,
8526
+ offset: 0
8527
+ },
8528
+ "mg/m³": {
8529
+ dimension: "concentration-mass",
8530
+ factor: 1e3,
8531
+ offset: 0
8532
+ },
8533
+ kg: {
8534
+ dimension: "mass",
8535
+ factor: 1,
8536
+ offset: 0
8537
+ },
8538
+ g: {
8539
+ dimension: "mass",
8540
+ factor: .001,
8541
+ offset: 0
8542
+ },
8543
+ lb: {
8544
+ dimension: "mass",
8545
+ factor: .453592,
8546
+ offset: 0
8547
+ },
8548
+ oz: {
8549
+ dimension: "mass",
8550
+ factor: .0283495,
8551
+ offset: 0
8552
+ },
8553
+ "%": {
8554
+ dimension: "percentage",
8555
+ factor: 1,
8556
+ offset: 0
8557
+ },
8558
+ L: {
8559
+ dimension: "volume",
8560
+ factor: 1,
8561
+ offset: 0
8562
+ },
8563
+ mL: {
8564
+ dimension: "volume",
8565
+ factor: .001,
8566
+ offset: 0
8567
+ },
8568
+ gal: {
8569
+ dimension: "volume",
8570
+ factor: 3.785411784,
8571
+ offset: 0
8572
+ },
8573
+ s: {
8574
+ dimension: "duration",
8575
+ factor: 1,
8576
+ offset: 0
8577
+ },
8578
+ ms: {
8579
+ dimension: "duration",
8580
+ factor: .001,
8581
+ offset: 0
8582
+ },
8583
+ min: {
8584
+ dimension: "duration",
8585
+ factor: 60,
8586
+ offset: 0
8587
+ },
8588
+ h: {
8589
+ dimension: "duration",
8590
+ factor: 3600,
8591
+ offset: 0
8592
+ },
8593
+ d: {
8594
+ dimension: "duration",
8595
+ factor: 86400,
8596
+ offset: 0
8597
+ },
8598
+ B: {
8599
+ dimension: "information",
8600
+ factor: 1,
8601
+ offset: 0
8602
+ },
8603
+ KiB: {
8604
+ dimension: "information",
8605
+ factor: 1024,
8606
+ offset: 0
8607
+ },
8608
+ MiB: {
8609
+ dimension: "information",
8610
+ factor: 1024 ** 2,
8611
+ offset: 0
8612
+ },
8613
+ GiB: {
8614
+ dimension: "information",
8615
+ factor: 1024 ** 3,
8616
+ offset: 0
8617
+ },
8618
+ TiB: {
8619
+ dimension: "information",
8620
+ factor: 1024 ** 4,
8621
+ offset: 0
8622
+ },
8623
+ kB: {
8624
+ dimension: "information",
8625
+ factor: 1e3,
8626
+ offset: 0
8627
+ },
8628
+ MB: {
8629
+ dimension: "information",
8630
+ factor: 1e6,
8631
+ offset: 0
8632
+ },
8633
+ GB: {
8634
+ dimension: "information",
8635
+ factor: 1e9,
8636
+ offset: 0
8637
+ },
8638
+ "°": {
8639
+ dimension: "angle",
8640
+ factor: 1,
8641
+ offset: 0
8642
+ },
8643
+ dBFS: {
8644
+ dimension: "sound-level",
8645
+ factor: 1,
8646
+ offset: 0
8647
+ },
8648
+ dBm: {
8649
+ dimension: "signal-level",
8650
+ factor: 1,
8651
+ offset: 0
8652
+ },
8653
+ mired: {
8654
+ dimension: "colour-temperature",
8655
+ factor: 1,
8656
+ offset: 0
8657
+ },
8658
+ kbps: {
8659
+ dimension: "bitrate",
8660
+ factor: 1,
8661
+ offset: 0
8662
+ },
8663
+ Mbps: {
8664
+ dimension: "bitrate",
8665
+ factor: 1e3,
8666
+ offset: 0
8667
+ },
8668
+ fps: {
8669
+ dimension: "rate",
8670
+ factor: 1,
8671
+ offset: 0
8672
+ }
8673
+ };
8674
+ //#endregion
8675
+ //#region src/units/convert.ts
8676
+ /**
8677
+ * Pure unit-conversion API over `UNIT_TABLE`. No dependencies, no side effects.
8678
+ *
8679
+ * Conversion is refused (throws `UnitConversionError`, or returns `undefined`
8680
+ * from the `try*` variant) whenever a unit is unknown or the two units belong
8681
+ * to different dimensions — cross-dimension conversion is deliberately
8682
+ * impossible so a render override can never make the value and its unit label
8683
+ * disagree.
8684
+ *
8685
+ * Callers pass units through `normalizeUnit` (`../device/source-info.ts`) BEFORE
8686
+ * calling here — the table keys only canonical spellings.
8687
+ */
8688
+ /** Thrown by `convertUnit` when a conversion is refused. Carries the offending
8689
+ * units + a machine-readable `reason` for callers that branch on the cause. */
8690
+ var UnitConversionError = class extends Error {
8691
+ from;
8692
+ to;
8693
+ reason;
8694
+ constructor(from, to, reason) {
8695
+ super(`[units] cannot convert '${from}' → '${to}': ${reason}`);
8696
+ this.name = "UnitConversionError";
8697
+ this.from = from;
8698
+ this.to = to;
8699
+ this.reason = reason;
8700
+ }
8701
+ };
8702
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
8703
+ function specFor(unit) {
8704
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
8705
+ }
8706
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
8707
+ function unitDimension(unit) {
8708
+ return specFor(unit)?.dimension;
8709
+ }
8710
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
8711
+ function toCanonical(value, spec) {
8712
+ return value * spec.factor + spec.offset;
8713
+ }
8714
+ /** Canonical-unit value → the unit described by `spec`. */
8715
+ function fromCanonical(canonical, spec) {
8716
+ return (canonical - spec.offset) / spec.factor;
8717
+ }
8718
+ /**
8719
+ * Convert `value` from unit `from` to unit `to`. Identity when `from === to`.
8720
+ * Throws `UnitConversionError` when either unit is unknown or the units belong
8721
+ * to different dimensions.
8722
+ */
8723
+ function convertUnit(value, from, to) {
8724
+ if (from === to) return value;
8725
+ const fromSpec = specFor(from);
8726
+ if (!fromSpec) throw new UnitConversionError(from, to, "unknown-from");
8727
+ const toSpec = specFor(to);
8728
+ if (!toSpec) throw new UnitConversionError(from, to, "unknown-to");
8729
+ if (fromSpec.dimension !== toSpec.dimension) throw new UnitConversionError(from, to, "cross-dimension");
8730
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
8731
+ }
8732
+ /**
8733
+ * Graceful variant for the render path: returns the converted value, or
8734
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
8735
+ * Never throws — a render caller falls back to the raw value + source unit.
8736
+ */
8737
+ function tryConvertUnit(value, from, to) {
8738
+ if (from === to) return value;
8739
+ const fromSpec = specFor(from);
8740
+ if (!fromSpec) return void 0;
8741
+ const toSpec = specFor(to);
8742
+ if (!toSpec) return void 0;
8743
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
8744
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
8745
+ }
8746
+ /**
8747
+ * Whether `value` can be converted between `from` and `to` (same dimension,
8748
+ * both known). Cheap boolean probe for the UI's same-dimension unit picker and
8749
+ * the resolver's convertibility branch — compute ONCE so a unit label and its
8750
+ * value math can never disagree.
8751
+ */
8752
+ function canConvertUnit(from, to) {
8753
+ if (from === to) return true;
8754
+ const fromDim = unitDimension(from);
8755
+ if (fromDim === void 0) return false;
8756
+ return fromDim === unitDimension(to);
8757
+ }
8758
+ /**
8759
+ * Every known unit spelling in a dimension (table insertion order — canonical
8760
+ * unit first). Powers the UI's same-dimension unit picker.
8761
+ */
8762
+ /**
8763
+ * Convert a difference, not a reading. Affine units (°F) shift an absolute
8764
+ * value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
8765
+ */
8766
+ function tryConvertDelta(value, from, to) {
8767
+ if (from === to) return value;
8768
+ const origin = tryConvertUnit(0, from, to);
8769
+ const shifted = tryConvertUnit(value, from, to);
8770
+ if (origin === void 0 || shifted === void 0) return void 0;
8771
+ return shifted - origin;
8772
+ }
8773
+ function unitsForDimension(dimension) {
8774
+ const out = [];
8775
+ for (const [unit, spec] of Object.entries(UNIT_TABLE)) if (spec.dimension === dimension) out.push(unit);
8776
+ return out;
8777
+ }
8778
+ //#endregion
8779
+ //#region src/units/quantity-input.ts
8780
+ /**
8781
+ * A number a person types for a physical quantity — the input side of the
8782
+ * value + unit contract (D972).
8783
+ *
8784
+ * Readings are stored in the base unit of their quantity (D944); a cap method
8785
+ * param is stored in the unit its input schema DECLARES (`paramMeta`). The
8786
+ * wire pair {@link QuantityValue} says "this number is expressed in this
8787
+ * unit"; {@link convertQuantityInput} brings it to the stored unit and refuses,
8788
+ * with a typed {@link QuantityUnitError}, a unit that cannot be that quantity.
8789
+ */
8790
+ /** A unit that cannot be applied to the number it was given with. */
8791
+ var QuantityUnitError = class extends Error {
8792
+ code;
8793
+ constructor(code, message) {
8794
+ super(`[${code}] ${message}`);
8795
+ this.name = "QuantityUnitError";
8796
+ this.code = code;
8797
+ }
8798
+ };
8799
+ /** The wire pair: `value` is expressed in `unit`. */
8800
+ var QuantityValueSchema = zod.z.object({
8801
+ value: zod.z.number(),
8802
+ unit: zod.z.string().min(1)
8803
+ });
8804
+ function isQuantityValue(candidate) {
8805
+ return QuantityValueSchema.safeParse(candidate).success;
8806
+ }
8807
+ /**
8808
+ * `value` expressed in `unit`, brought to `target`. Throws
8809
+ * {@link QuantityUnitError} for a non-finite value, an unknown unit or a unit
8810
+ * of another dimension — never guesses.
8811
+ */
8812
+ function convertQuantityInput(input) {
8813
+ if (!Number.isFinite(input.value)) throw new QuantityUnitError("non-finite", `${String(input.value)} is not a finite number`);
8814
+ const unit = normalizeUnit(input.unit);
8815
+ if (unit === void 0 || unitDimension(unit) === void 0) throw new QuantityUnitError("unknown-unit", `unknown unit "${input.unit}"`);
8816
+ const converted = tryConvertUnit(input.value, unit, input.target);
8817
+ if (converted === void 0) throw new QuantityUnitError("cross-dimension", `"${input.unit}" is not a unit of ${input.target}'s quantity`);
8818
+ return Number(converted.toPrecision(12));
8819
+ }
8820
+ /**
8821
+ * Zod `.meta()` bag for a cap METHOD param that is a physical quantity — the
8822
+ * input-side twin of `readingMeta`. The value is carried in `unit` (a gram
8823
+ * portion stays in grams); the unit must belong to the quantity, checked once
8824
+ * when the cap is declared.
8825
+ */
8826
+ function paramMeta(declared) {
8827
+ const base = require_sleep.baseUnitOf(declared.quantity);
8828
+ if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
8829
+ if (tryConvertUnit(1, declared.unit, base) === void 0) throw new QuantityUnitError("cross-dimension", `"${declared.unit}" is not a unit of ${declared.quantity}`);
8830
+ return {
8831
+ quantity: declared.quantity,
8832
+ unit: declared.unit
8833
+ };
8834
+ }
8835
+ var ParamMetaBagSchema = zod.z.object({
8836
+ quantity: require_sleep.ReadingQuantitySchema,
8837
+ unit: zod.z.string().min(1)
8838
+ });
8839
+ function metaOf(schema) {
8840
+ const parsed = ParamMetaBagSchema.safeParse(zod.z.globalRegistry.get(schema));
8841
+ return parsed.success ? parsed.data : void 0;
8842
+ }
8843
+ /** The `paramMeta` on a method input field (or its unwrapped leaf), if any. */
8844
+ function paramMetaOf(field) {
8845
+ return metaOf(field) ?? metaOf(unwrapSchema(field));
8846
+ }
8847
+ //#endregion
8063
8848
  //#region src/automation/recipe.ts
8064
8849
  /**
8065
8850
  * The automation RECIPE — what a visually-authored automation is, before it
@@ -8121,8 +8906,10 @@ var AutomationConditionLeafSchema = zod.z.object({
8121
8906
  zod.z.string(),
8122
8907
  zod.z.number(),
8123
8908
  zod.z.boolean(),
8124
- zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]))
8125
- ])
8909
+ zod.z.array(zod.z.union([zod.z.string(), zod.z.number()])),
8910
+ QuantityValueSchema
8911
+ ]),
8912
+ unit: zod.z.string().min(1).optional()
8126
8913
  });
8127
8914
  var AutomationConditionStationaryPresentSchema = zod.z.object({
8128
8915
  kind: zod.z.literal("stationary-present"),
@@ -8275,7 +9062,13 @@ var AutomationCapStepSchema = zod.z.object({
8275
9062
  /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
8276
9063
  * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
8277
9064
  * the expression leaf. */
8278
- args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
9065
+ args: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
9066
+ /**
9067
+ * The unit the person picked per arg (display only). `args` always hold the
9068
+ * value in the unit the method's input DECLARES (`paramMeta`); on a write an
9069
+ * arg may be a {@link QuantityValue}, which the save boundary converts.
9070
+ */
9071
+ argUnits: zod.z.record(zod.z.string(), zod.z.string().min(1)).optional()
8279
9072
  });
8280
9073
  /**
8281
9074
  * The steps an internal Action may contain: cap and wait, and nothing that can
@@ -9855,6 +10648,14 @@ function evalNode(cond, path, reader, now, out) {
9855
10648
  expected: toTraceValue(cond.value),
9856
10649
  observed: toTraceValue(observed)
9857
10650
  };
10651
+ if (isQuantityValue(cond.value)) {
10652
+ out.push({
10653
+ ...base,
10654
+ outcome: "fail",
10655
+ reason: `${cond.fieldPath} expected value's unit was never converted → fail`
10656
+ });
10657
+ return false;
10658
+ }
9858
10659
  if (observed === void 0) {
9859
10660
  out.push({
9860
10661
  ...base,
@@ -10705,6 +11506,242 @@ function buildActionSteps(steps, vars, deps) {
10705
11506
  });
10706
11507
  }
10707
11508
  //#endregion
11509
+ //#region src/units/quantity-units.ts
11510
+ /**
11511
+ * Which units a person may pick for a number of a given quantity. The
11512
+ * quantity's stored base unit comes first (D944), then every other unit of the
11513
+ * same dimension in table order. A quantity with no unit (`none`, `count`,
11514
+ * `index`, `ratio`, `device`) offers nothing: the number is just a number.
11515
+ */
11516
+ /** The unit a reading of `quantity` is stored in, or `undefined` when it has none. */
11517
+ function baseUnitForQuantity(quantity) {
11518
+ return require_sleep.baseUnitOf(quantity);
11519
+ }
11520
+ /**
11521
+ * The units a person can enter a number of `quantity` in — base unit first.
11522
+ * Empty when the quantity carries no unit.
11523
+ */
11524
+ function unitsForQuantity(quantity) {
11525
+ const base = baseUnitForQuantity(quantity);
11526
+ if (base === void 0) return [];
11527
+ const dimension = unitDimension(base);
11528
+ if (dimension === void 0) return [base];
11529
+ return [base, ...unitsForDimension(dimension).filter((unit) => unit !== base)];
11530
+ }
11531
+ //#endregion
11532
+ //#region src/automation/normalize-units.ts
11533
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
11534
+ var UnitWarningSchema = zod.z.object({
11535
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
11536
+ path: zod.z.string(),
11537
+ deviceId: zod.z.number().int().optional(),
11538
+ field: zod.z.string(),
11539
+ value: zod.z.number(),
11540
+ assumedUnit: zod.z.string(),
11541
+ message: zod.z.string()
11542
+ });
11543
+ function objectInput(schema) {
11544
+ const inner = unwrapSchema(schema);
11545
+ return inner instanceof zod.z.ZodObject ? inner : void 0;
11546
+ }
11547
+ /** A resolver over capability definitions (the generated registry, or a fake). */
11548
+ function capUnitResolver(defs) {
11549
+ const byName = new Map(defs.map((def) => [def.name, def]));
11550
+ return {
11551
+ fieldQuantity(cap, fieldPath) {
11552
+ const status = byName.get(cap)?.status;
11553
+ if (status === void 0) return void 0;
11554
+ return [...enumerateSchemaFields(status.schema), ...status.itemArray === void 0 ? [] : enumerateItemArrayFields(status.itemArray)].find((f) => f.path === fieldPath)?.quantity;
11555
+ },
11556
+ paramMeta(cap, method, arg) {
11557
+ const input = byName.get(cap)?.methods[method]?.input;
11558
+ if (input === void 0) return void 0;
11559
+ const field = objectInput(input)?.shape[arg];
11560
+ return field === void 0 ? void 0 : paramMetaOf(field);
11561
+ }
11562
+ };
11563
+ }
11564
+ function pickedUnit(raw) {
11565
+ return normalizeUnit(raw) ?? raw;
11566
+ }
11567
+ function hasUnitChoice(quantity) {
11568
+ return unitsForQuantity(quantity).length > 1;
11569
+ }
11570
+ function bareWarning(input) {
11571
+ return {
11572
+ ...input,
11573
+ message: `${input.field}: the bare number ${input.value} was taken as ${input.assumedUnit} — send { value, unit } to state the unit`
11574
+ };
11575
+ }
11576
+ function checkPickedUnit(unit, target) {
11577
+ convertQuantityInput({
11578
+ value: 1,
11579
+ unit,
11580
+ target
11581
+ });
11582
+ }
11583
+ function normalizeLeaf(leaf, resolver, path) {
11584
+ const quantity = resolver.fieldQuantity(leaf.cap, leaf.fieldPath);
11585
+ const base = quantity === void 0 ? void 0 : baseUnitForQuantity(quantity);
11586
+ const wantsUnit = isQuantityValue(leaf.value) || leaf.unit !== void 0;
11587
+ if (base === void 0 || quantity === void 0) {
11588
+ if (wantsUnit) throw new QuantityUnitError("no-quantity", `${leaf.cap}.${leaf.fieldPath} is not a quantity — it takes no unit`);
11589
+ return {
11590
+ leaf,
11591
+ warnings: []
11592
+ };
11593
+ }
11594
+ if (isQuantityValue(leaf.value)) {
11595
+ const converted = convertQuantityInput({
11596
+ value: leaf.value.value,
11597
+ unit: leaf.value.unit,
11598
+ target: base
11599
+ });
11600
+ return {
11601
+ leaf: {
11602
+ ...leaf,
11603
+ value: converted,
11604
+ unit: pickedUnit(leaf.value.unit)
11605
+ },
11606
+ warnings: []
11607
+ };
11608
+ }
11609
+ if (typeof leaf.value !== "number") return {
11610
+ leaf,
11611
+ warnings: []
11612
+ };
11613
+ if (leaf.unit !== void 0) {
11614
+ checkPickedUnit(leaf.unit, base);
11615
+ return {
11616
+ leaf,
11617
+ warnings: []
11618
+ };
11619
+ }
11620
+ if (!hasUnitChoice(quantity)) return {
11621
+ leaf,
11622
+ warnings: []
11623
+ };
11624
+ return {
11625
+ leaf,
11626
+ warnings: [bareWarning({
11627
+ path,
11628
+ deviceId: leaf.deviceId,
11629
+ field: leaf.fieldPath,
11630
+ value: leaf.value,
11631
+ assumedUnit: base
11632
+ })]
11633
+ };
11634
+ }
11635
+ /** Normalise every leaf of a condition tree. Throws {@link QuantityUnitError}. */
11636
+ function normalizeConditionUnits(condition, resolver, path = "conditions") {
11637
+ switch (condition.kind) {
11638
+ case "condition": {
11639
+ const { leaf, warnings } = normalizeLeaf(condition, resolver, path);
11640
+ return {
11641
+ value: leaf,
11642
+ warnings
11643
+ };
11644
+ }
11645
+ case "not": {
11646
+ const inner = normalizeConditionUnits(condition.child, resolver, `${path}.child`);
11647
+ return {
11648
+ value: {
11649
+ ...condition,
11650
+ child: inner.value
11651
+ },
11652
+ warnings: inner.warnings
11653
+ };
11654
+ }
11655
+ case "all":
11656
+ case "any": {
11657
+ const children = condition.children.map((child, i) => normalizeConditionUnits(child, resolver, `${path}.children.${i}`));
11658
+ return {
11659
+ value: {
11660
+ ...condition,
11661
+ children: children.map((c) => c.value)
11662
+ },
11663
+ warnings: children.flatMap((c) => c.warnings)
11664
+ };
11665
+ }
11666
+ default: return {
11667
+ value: condition,
11668
+ warnings: []
11669
+ };
11670
+ }
11671
+ }
11672
+ function normalizeCapStep(step, resolver, path) {
11673
+ const deviceId = typeof step.deviceId === "number" ? step.deviceId : void 0;
11674
+ const args = { ...step.args };
11675
+ const argUnits = { ...step.argUnits };
11676
+ const warnings = [];
11677
+ for (const [arg, picked] of Object.entries(step.argUnits ?? {})) {
11678
+ const meta = resolver.paramMeta(step.cap, step.method, arg);
11679
+ if (meta === void 0) throw new QuantityUnitError("no-quantity", `${step.cap}.${step.method}(${arg}) is not a quantity — it takes no unit`);
11680
+ checkPickedUnit(picked, meta.unit);
11681
+ }
11682
+ for (const [arg, given] of Object.entries(step.args ?? {})) {
11683
+ const meta = resolver.paramMeta(step.cap, step.method, arg);
11684
+ if (meta === void 0) continue;
11685
+ if (isQuantityValue(given)) {
11686
+ args[arg] = convertQuantityInput({
11687
+ value: given.value,
11688
+ unit: given.unit,
11689
+ target: meta.unit
11690
+ });
11691
+ argUnits[arg] = pickedUnit(given.unit);
11692
+ } else if (typeof given === "number" && argUnits[arg] === void 0 && hasUnitChoice(meta.quantity)) warnings.push(bareWarning({
11693
+ path: `${path}.args.${arg}`,
11694
+ ...deviceId === void 0 ? {} : { deviceId },
11695
+ field: arg,
11696
+ value: given,
11697
+ assumedUnit: meta.unit
11698
+ }));
11699
+ }
11700
+ const { argUnits: _dropped, ...rest } = step;
11701
+ return {
11702
+ value: {
11703
+ ...rest,
11704
+ ...step.args === void 0 ? {} : { args },
11705
+ ...Object.keys(argUnits).length > 0 ? { argUnits } : {}
11706
+ },
11707
+ warnings
11708
+ };
11709
+ }
11710
+ /**
11711
+ * Normalise the cap steps of an action list (an internal Action's steps, or a
11712
+ * recipe's actions). Other step kinds pass through. Throws {@link QuantityUnitError}.
11713
+ */
11714
+ function normalizeActionUnits(actions, resolver, path = "steps") {
11715
+ const out = [];
11716
+ const warnings = [];
11717
+ actions.forEach((action, i) => {
11718
+ if (action.kind !== "cap") {
11719
+ out.push(action);
11720
+ return;
11721
+ }
11722
+ const normalized = normalizeCapStep(action, resolver, `${path}.${i}`);
11723
+ out.push(normalized.value);
11724
+ warnings.push(...normalized.warnings);
11725
+ });
11726
+ return {
11727
+ value: out,
11728
+ warnings
11729
+ };
11730
+ }
11731
+ /** Normalise a whole recipe: its condition tree and its actions. */
11732
+ function normalizeRecipeUnits(recipe, resolver) {
11733
+ const conditions = recipe.conditions === void 0 ? void 0 : normalizeConditionUnits(recipe.conditions, resolver, "conditions");
11734
+ const actions = normalizeActionUnits(recipe.actions, resolver, "actions");
11735
+ return {
11736
+ value: {
11737
+ ...recipe,
11738
+ ...conditions === void 0 ? {} : { conditions: conditions.value },
11739
+ actions: actions.value
11740
+ },
11741
+ warnings: [...conditions?.warnings ?? [], ...actions.warnings]
11742
+ };
11743
+ }
11744
+ //#endregion
10708
11745
  //#region src/capabilities/notification-output.cap.ts
10709
11746
  /**
10710
11747
  * notification-output — canonical, capability-gated notification delivery.
@@ -15199,77 +16236,6 @@ function kebabToCamel(s) {
15199
16236
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
15200
16237
  }
15201
16238
  //#endregion
15202
- //#region src/device/schema-fields.ts
15203
- /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
15204
- * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
15205
- function unwrapSchema(schema) {
15206
- let s = schema;
15207
- while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
15208
- return s;
15209
- }
15210
- function isMetaBag(value) {
15211
- return typeof value === "object" && value !== null;
15212
- }
15213
- function metaQuantity(schema) {
15214
- const bag = zod.z.globalRegistry.get(schema);
15215
- if (isMetaBag(bag) && require_sleep.isReadingQuantity(bag.quantity)) return bag.quantity;
15216
- }
15217
- /** Quantity declared with `readingMeta` on the field or on the unwrapped leaf. */
15218
- function quantityOfField(schema) {
15219
- return metaQuantity(schema) ?? metaQuantity(unwrapSchema(schema));
15220
- }
15221
- function leafKind(schema) {
15222
- const s = unwrapSchema(schema);
15223
- if (s instanceof zod.z.ZodEnum) return {
15224
- kind: "enum",
15225
- enumValues: s.options
15226
- };
15227
- if (s instanceof zod.z.ZodNumber) return { kind: "number" };
15228
- if (s instanceof zod.z.ZodString) return { kind: "string" };
15229
- if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
15230
- return null;
15231
- }
15232
- /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
15233
- * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
15234
- * so fields with null live values are still offered. Arrays / unknown shapes
15235
- * are skipped — never throws. */
15236
- function enumerateSchemaFields(schema, prefix = "") {
15237
- const root = unwrapSchema(schema);
15238
- if (!(root instanceof zod.z.ZodObject)) return [];
15239
- const out = [];
15240
- const shape = root.shape;
15241
- for (const [key, field] of Object.entries(shape)) {
15242
- const path = prefix ? `${prefix}.${key}` : key;
15243
- const inner = unwrapSchema(field);
15244
- if (inner instanceof zod.z.ZodObject) {
15245
- out.push(...enumerateSchemaFields(inner, path));
15246
- continue;
15247
- }
15248
- const leaf = leafKind(field);
15249
- if (leaf) {
15250
- const quantity = quantityOfField(field);
15251
- out.push({
15252
- path,
15253
- kind: leaf.kind,
15254
- ...leaf.enumValues ? { enumValues: leaf.enumValues } : {},
15255
- ...quantity !== void 0 ? { quantity } : {}
15256
- });
15257
- }
15258
- }
15259
- return out;
15260
- }
15261
- /** Enumerate the per-item wireable fields of an item-array cap (see
15262
- * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
15263
- * `keyField` (the key comes from the link's `itemKey`, never from a wired
15264
- * source), each tagged `item: true` so the authoring UI collects an
15265
- * `itemKey` alongside the field. Never throws. */
15266
- function enumerateItemArrayFields(itemArray) {
15267
- return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
15268
- ...f,
15269
- item: true
15270
- }));
15271
- }
15272
- //#endregion
15273
16239
  //#region src/composition/state-fields.ts
15274
16240
  /**
15275
16241
  * What a capability's state field IS: kind, nullability, whether required. The
@@ -21163,192 +22129,6 @@ function parseAdminBase(baseUrl) {
21163
22129
  };
21164
22130
  }
21165
22131
  //#endregion
21166
- //#region src/device/source-info.ts
21167
- /**
21168
- * Identity envelope for a device's upstream-system metadata.
21169
- *
21170
- * Two jobs:
21171
- *
21172
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
21173
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
21174
- * WS-Discovery endpoint UUID, …). Provider code keeps a
21175
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
21176
- * without round-tripping through `stableId` / `id` mappings.
21177
- *
21178
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
21179
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
21180
- * for vendors where MAC is the natural key). For systems where the
21181
- * live dispatch key can be renamed (HA `entity_id` can be edited),
21182
- * providers detect renames by matching `uniqueId` and patch `id`
21183
- * in-place so dispatch keeps working.
21184
- *
21185
- * **Display unit and precision are NOT stored here.** They are a read-time
21186
- * preference (D944). A polymorphic reading stores its `quantity` here once,
21187
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
21188
- * neither is repeated on each sample. The `device_class` tag is an
21189
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21190
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
21191
- *
21192
- * The `raw` escape hatch carries provider-specific attributes that don't
21193
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
21194
- * details, …). Providers control what lands here.
21195
- */
21196
- var SourceInfoSchema = zod.z.object({
21197
- /** Live dispatch key — mutable when the source system allows rename. */
21198
- id: zod.z.string(),
21199
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
21200
- system: zod.z.string(),
21201
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
21202
- * Used to detect rename when `id` changes. */
21203
- uniqueId: zod.z.string().optional(),
21204
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
21205
- quantity: require_sleep.ReadingQuantitySchema.optional(),
21206
- /** Date / time / datetime shape of a string reading, written once at adopt. */
21207
- dateTimeShape: zod.z.enum([
21208
- "date",
21209
- "time",
21210
- "datetime"
21211
- ]).optional(),
21212
- /** Provider-specific extras that don't fit the structured slots.
21213
- * Whitelist what goes here per provider — do NOT dump entire HA
21214
- * attribute blobs (would explode the meta JSON column). */
21215
- raw: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
21216
- });
21217
- /**
21218
- * Build the synthetic SourceInfo every existing provider falls back to
21219
- * when no upstream value has been persisted. Keeps non-migrated providers
21220
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
21221
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
21222
- * id. Real upstream identity replaces this once a provider migrates and
21223
- * calls `updateSourceInfo()` with concrete values.
21224
- */
21225
- function synthesizeSourceInfo(input) {
21226
- return {
21227
- id: input.stableId,
21228
- system: input.addonId
21229
- };
21230
- }
21231
- /**
21232
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
21233
- * patch values are ignored (no field clobber); to clear a field
21234
- * explicitly, providers omit it from the patch and rebuild from scratch
21235
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
21236
- * bother with a `null`-means-delete sentinel.
21237
- *
21238
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
21239
- * merges over existing `raw` keys instead of replacing the whole bag.
21240
- */
21241
- function mergeSourceInfo(prev, patch) {
21242
- const next = { ...prev };
21243
- for (const [k, v] of Object.entries(patch)) {
21244
- if (v === void 0) continue;
21245
- if (k === "raw" && typeof v === "object" && v !== null) {
21246
- next.raw = {
21247
- ...prev.raw,
21248
- ...v
21249
- };
21250
- continue;
21251
- }
21252
- next[k] = v;
21253
- }
21254
- return next;
21255
- }
21256
- /**
21257
- * Coerce common unit-of-measurement string variants to a canonical form
21258
- * so the UI's display templates can rely on stable strings. Pure
21259
- * passthrough for unknown units — kept conservative on purpose, the goal
21260
- * is to normalise the long tail of HA `unit_of_measurement` values that
21261
- * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
21262
- *
21263
- * Extend the map as we encounter real-world data. Returning the raw
21264
- * input for unknown values is intentional: providers should never
21265
- * lose information silently here.
21266
- */
21267
- function normalizeUnit(raw) {
21268
- if (raw == null) return void 0;
21269
- const trimmed = raw.trim();
21270
- if (trimmed === "") return void 0;
21271
- const lower = trimmed.toLowerCase();
21272
- if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
21273
- if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
21274
- if (lower === "k" || lower === "kelvin") return "K";
21275
- if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
21276
- if (lower === "lx" || lower === "lux") return "lx";
21277
- if (lower === "hpa" || lower === "hectopascal") return "hPa";
21278
- if (lower === "kpa") return "kPa";
21279
- if (lower === "pa") return "Pa";
21280
- if (lower === "mbar" || lower === "millibar") return "mbar";
21281
- if (lower === "w" || lower === "watt") return "W";
21282
- if (lower === "kw" || lower === "kilowatt") return "kW";
21283
- if (lower === "wh" || lower === "watt-hour") return "Wh";
21284
- if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
21285
- if (lower === "v" || lower === "volt") return "V";
21286
- if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
21287
- if (lower === "ma" || lower === "milliampere") return "mA";
21288
- if (lower === "va") return "VA";
21289
- if (lower === "ppm") return "ppm";
21290
- if (lower === "ppb") return "ppb";
21291
- if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
21292
- if (lower === "km/h" || lower === "kph") return "km/h";
21293
- if (lower === "mph") return "mph";
21294
- if (lower === "m/s") return "m/s";
21295
- if (lower === "m") return "m";
21296
- if (lower === "m²" || lower === "m2") return "m²";
21297
- if (lower === "mm") return "mm";
21298
- if (lower === "cm") return "cm";
21299
- if (lower === "km") return "km";
21300
- if (lower === "in" || lower === "inch" || lower === "inches") return "in";
21301
- if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
21302
- if (lower === "kg") return "kg";
21303
- if (lower === "g") return "g";
21304
- if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
21305
- if (lower === "ml") return "mL";
21306
- if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
21307
- if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
21308
- if (lower === "ms") return "ms";
21309
- if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
21310
- if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
21311
- if (lower === "d" || lower === "day" || lower === "days") return "d";
21312
- if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
21313
- if (lower === "kib") return "KiB";
21314
- if (lower === "mib") return "MiB";
21315
- if (lower === "gib") return "GiB";
21316
- if (lower === "tib") return "TiB";
21317
- if (lower === "kb") return "kB";
21318
- if (lower === "mb") return "MB";
21319
- if (lower === "gb") return "GB";
21320
- if (lower === "dbm") return "dBm";
21321
- if (lower === "dbfs") return "dBFS";
21322
- if (lower === "inhg") return "inHg";
21323
- if (lower === "mm/h") return "mm/h";
21324
- if (lower === "w/m²" || lower === "w/m2") return "W/m²";
21325
- if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
21326
- if (lower === "kbps") return "kbps";
21327
- if (lower === "fps") return "fps";
21328
- return trimmed;
21329
- }
21330
- /**
21331
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
21332
- * blob is a free-form `Record<string, unknown>` shared with hardware-
21333
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
21334
- * lives under a single nested key to keep it composable with those.
21335
- */
21336
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
21337
- /**
21338
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
21339
- * with Zod. Returns `null` when the blob is missing or the nested
21340
- * `sourceInfo` key fails validation — callers fall back to the
21341
- * synthetic default in that case (see `synthesizeSourceInfo`).
21342
- */
21343
- function extractSourceInfoFromMetadata(metadata) {
21344
- if (!metadata) return null;
21345
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
21346
- if (!raw) return null;
21347
- const parsed = SourceInfoSchema.safeParse(raw);
21348
- if (!parsed.success) return null;
21349
- return parsed.data;
21350
- }
21351
- //#endregion
21352
22132
  //#region src/capabilities/device-discovery.cap.ts
21353
22133
  /**
21354
22134
  * device-discovery — device-scoped capability for parents that host /
@@ -65158,15 +65938,29 @@ var actionsCapability = {
65158
65938
  auth: "admin"
65159
65939
  }),
65160
65940
  getTraces: require_sleep.method(ActionIdInput, zod.z.object({ traces: zod.z.array(AutomationRunTraceSchema) }), { auth: "admin" }),
65161
- /** Internal actions only. A read-only (addon) entry is refused with `[read-only]`. */
65162
- create: require_sleep.method(zod.z.object({ action: ActionInputSchema }), zod.z.object({ action: ActionEntrySchema }), {
65941
+ /**
65942
+ * Internal actions only; a read-only (addon) entry is refused with `[read-only]`.
65943
+ * Create an internal action. A cap step arg that is a physical quantity may
65944
+ * be sent as `{ value, unit }` ("value expressed in unit"); the server
65945
+ * converts it to the unit the method's input declares, stores the bare
65946
+ * number plus the picked unit in `argUnits`, and refuses a unit of another
65947
+ * dimension (`[invalid-steps]`). A bare number is taken as the declared
65948
+ * unit and answered as a `warnings` entry (D972).
65949
+ */
65950
+ create: require_sleep.method(zod.z.object({ action: ActionInputSchema }), zod.z.object({
65951
+ action: ActionEntrySchema,
65952
+ warnings: zod.z.array(UnitWarningSchema).optional()
65953
+ }), {
65163
65954
  kind: "mutation",
65164
65955
  auth: "admin"
65165
65956
  }),
65166
65957
  update: require_sleep.method(zod.z.object({
65167
65958
  actionId: zod.z.string().min(1),
65168
65959
  action: ActionInputSchema.partial()
65169
- }), zod.z.object({ action: ActionEntrySchema }), {
65960
+ }), zod.z.object({
65961
+ action: ActionEntrySchema,
65962
+ warnings: zod.z.array(UnitWarningSchema).optional()
65963
+ }), {
65170
65964
  kind: "mutation",
65171
65965
  auth: "admin"
65172
65966
  }),
@@ -73343,6 +74137,14 @@ var PetFeederDeviceStatusSchema = zod.z.enum([
73343
74137
  "on_batteries"
73344
74138
  ]);
73345
74139
  var gramsPortion = zod.z.number().int().min(4).max(200);
74140
+ /** A portion REQUESTED of the feeder is a mass the method takes in GRAMS (the
74141
+ * hardware's own unit). The bounds are grams; a caller entering another unit
74142
+ * converts to grams first (`paramMeta`, D972). The STATUS fields above stay
74143
+ * plain grams — they are not readings in the kilogram base. */
74144
+ var gramsParam = gramsPortion.meta(paramMeta({
74145
+ quantity: "mass",
74146
+ unit: "g"
74147
+ }));
73346
74148
  var PetFeederStatusSchema = zod.z.object({
73347
74149
  /** Food in the TANK (hopper), as the feeder's own sensor reports it
73348
74150
  * (`state.food`). A coarse level code, not grams — `0` is empty. The
@@ -73431,9 +74233,9 @@ var petFeederCapability = {
73431
74233
  */
73432
74234
  feed: require_sleep.method(zod.z.object({
73433
74235
  deviceId: zod.z.number().int().nonnegative(),
73434
- grams: gramsPortion.optional(),
73435
- hopper1: gramsPortion.optional(),
73436
- hopper2: gramsPortion.optional()
74236
+ grams: gramsParam.optional(),
74237
+ hopper1: gramsParam.optional(),
74238
+ hopper2: gramsParam.optional()
73437
74239
  }), zod.z.void(), {
73438
74240
  kind: "mutation",
73439
74241
  auth: "protected"
@@ -82891,465 +83693,6 @@ function numericAccept() {
82891
83693
  return historyAccept("statistic").filter((field) => field.valueKind === "number");
82892
83694
  }
82893
83695
  //#endregion
82894
- //#region src/units/unit-table.ts
82895
- /**
82896
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
82897
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
82898
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
82899
- * decimals). K derives from `°C = K − 273.15`.
82900
- */
82901
- var UNIT_TABLE = {
82902
- "°C": {
82903
- dimension: "temperature",
82904
- factor: 1,
82905
- offset: 0
82906
- },
82907
- "°F": {
82908
- dimension: "temperature",
82909
- factor: 5 / 9,
82910
- offset: -160 / 9
82911
- },
82912
- K: {
82913
- dimension: "temperature",
82914
- factor: 1,
82915
- offset: -273.15
82916
- },
82917
- hPa: {
82918
- dimension: "pressure",
82919
- factor: 1,
82920
- offset: 0
82921
- },
82922
- kPa: {
82923
- dimension: "pressure",
82924
- factor: 10,
82925
- offset: 0
82926
- },
82927
- Pa: {
82928
- dimension: "pressure",
82929
- factor: .01,
82930
- offset: 0
82931
- },
82932
- mbar: {
82933
- dimension: "pressure",
82934
- factor: 1,
82935
- offset: 0
82936
- },
82937
- bar: {
82938
- dimension: "pressure",
82939
- factor: 1e3,
82940
- offset: 0
82941
- },
82942
- inHg: {
82943
- dimension: "pressure",
82944
- factor: 33.8639,
82945
- offset: 0
82946
- },
82947
- mmHg: {
82948
- dimension: "pressure",
82949
- factor: 1.33322,
82950
- offset: 0
82951
- },
82952
- psi: {
82953
- dimension: "pressure",
82954
- factor: 68.9476,
82955
- offset: 0
82956
- },
82957
- "m/s": {
82958
- dimension: "speed",
82959
- factor: 1,
82960
- offset: 0
82961
- },
82962
- "km/h": {
82963
- dimension: "speed",
82964
- factor: 1 / 3.6,
82965
- offset: 0
82966
- },
82967
- mph: {
82968
- dimension: "speed",
82969
- factor: .44704,
82970
- offset: 0
82971
- },
82972
- kn: {
82973
- dimension: "speed",
82974
- factor: .514444,
82975
- offset: 0
82976
- },
82977
- "mm/h": {
82978
- dimension: "precipitation-rate",
82979
- factor: 1,
82980
- offset: 0
82981
- },
82982
- "in/h": {
82983
- dimension: "precipitation-rate",
82984
- factor: 25.4,
82985
- offset: 0
82986
- },
82987
- m: {
82988
- dimension: "length",
82989
- factor: 1,
82990
- offset: 0
82991
- },
82992
- mm: {
82993
- dimension: "length",
82994
- factor: .001,
82995
- offset: 0
82996
- },
82997
- cm: {
82998
- dimension: "length",
82999
- factor: .01,
83000
- offset: 0
83001
- },
83002
- km: {
83003
- dimension: "length",
83004
- factor: 1e3,
83005
- offset: 0
83006
- },
83007
- in: {
83008
- dimension: "length",
83009
- factor: .0254,
83010
- offset: 0
83011
- },
83012
- ft: {
83013
- dimension: "length",
83014
- factor: .3048,
83015
- offset: 0
83016
- },
83017
- mi: {
83018
- dimension: "length",
83019
- factor: 1609.344,
83020
- offset: 0
83021
- },
83022
- "m²": {
83023
- dimension: "area",
83024
- factor: 1,
83025
- offset: 0
83026
- },
83027
- lx: {
83028
- dimension: "illuminance",
83029
- factor: 1,
83030
- offset: 0
83031
- },
83032
- "W/m²": {
83033
- dimension: "irradiance",
83034
- factor: 1,
83035
- offset: 0
83036
- },
83037
- W: {
83038
- dimension: "power",
83039
- factor: 1,
83040
- offset: 0
83041
- },
83042
- kW: {
83043
- dimension: "power",
83044
- factor: 1e3,
83045
- offset: 0
83046
- },
83047
- VA: {
83048
- dimension: "apparent-power",
83049
- factor: 1,
83050
- offset: 0
83051
- },
83052
- Wh: {
83053
- dimension: "energy",
83054
- factor: 1,
83055
- offset: 0
83056
- },
83057
- kWh: {
83058
- dimension: "energy",
83059
- factor: 1e3,
83060
- offset: 0
83061
- },
83062
- MWh: {
83063
- dimension: "energy",
83064
- factor: 1e6,
83065
- offset: 0
83066
- },
83067
- V: {
83068
- dimension: "voltage",
83069
- factor: 1,
83070
- offset: 0
83071
- },
83072
- mV: {
83073
- dimension: "voltage",
83074
- factor: .001,
83075
- offset: 0
83076
- },
83077
- A: {
83078
- dimension: "current",
83079
- factor: 1,
83080
- offset: 0
83081
- },
83082
- mA: {
83083
- dimension: "current",
83084
- factor: .001,
83085
- offset: 0
83086
- },
83087
- ppm: {
83088
- dimension: "concentration-volume",
83089
- factor: 1,
83090
- offset: 0
83091
- },
83092
- ppb: {
83093
- dimension: "concentration-volume",
83094
- factor: .001,
83095
- offset: 0
83096
- },
83097
- "µg/m³": {
83098
- dimension: "concentration-mass",
83099
- factor: 1,
83100
- offset: 0
83101
- },
83102
- "mg/m³": {
83103
- dimension: "concentration-mass",
83104
- factor: 1e3,
83105
- offset: 0
83106
- },
83107
- kg: {
83108
- dimension: "mass",
83109
- factor: 1,
83110
- offset: 0
83111
- },
83112
- g: {
83113
- dimension: "mass",
83114
- factor: .001,
83115
- offset: 0
83116
- },
83117
- lb: {
83118
- dimension: "mass",
83119
- factor: .453592,
83120
- offset: 0
83121
- },
83122
- oz: {
83123
- dimension: "mass",
83124
- factor: .0283495,
83125
- offset: 0
83126
- },
83127
- "%": {
83128
- dimension: "percentage",
83129
- factor: 1,
83130
- offset: 0
83131
- },
83132
- L: {
83133
- dimension: "volume",
83134
- factor: 1,
83135
- offset: 0
83136
- },
83137
- mL: {
83138
- dimension: "volume",
83139
- factor: .001,
83140
- offset: 0
83141
- },
83142
- gal: {
83143
- dimension: "volume",
83144
- factor: 3.785411784,
83145
- offset: 0
83146
- },
83147
- s: {
83148
- dimension: "duration",
83149
- factor: 1,
83150
- offset: 0
83151
- },
83152
- ms: {
83153
- dimension: "duration",
83154
- factor: .001,
83155
- offset: 0
83156
- },
83157
- min: {
83158
- dimension: "duration",
83159
- factor: 60,
83160
- offset: 0
83161
- },
83162
- h: {
83163
- dimension: "duration",
83164
- factor: 3600,
83165
- offset: 0
83166
- },
83167
- d: {
83168
- dimension: "duration",
83169
- factor: 86400,
83170
- offset: 0
83171
- },
83172
- B: {
83173
- dimension: "information",
83174
- factor: 1,
83175
- offset: 0
83176
- },
83177
- KiB: {
83178
- dimension: "information",
83179
- factor: 1024,
83180
- offset: 0
83181
- },
83182
- MiB: {
83183
- dimension: "information",
83184
- factor: 1024 ** 2,
83185
- offset: 0
83186
- },
83187
- GiB: {
83188
- dimension: "information",
83189
- factor: 1024 ** 3,
83190
- offset: 0
83191
- },
83192
- TiB: {
83193
- dimension: "information",
83194
- factor: 1024 ** 4,
83195
- offset: 0
83196
- },
83197
- kB: {
83198
- dimension: "information",
83199
- factor: 1e3,
83200
- offset: 0
83201
- },
83202
- MB: {
83203
- dimension: "information",
83204
- factor: 1e6,
83205
- offset: 0
83206
- },
83207
- GB: {
83208
- dimension: "information",
83209
- factor: 1e9,
83210
- offset: 0
83211
- },
83212
- "°": {
83213
- dimension: "angle",
83214
- factor: 1,
83215
- offset: 0
83216
- },
83217
- dBFS: {
83218
- dimension: "sound-level",
83219
- factor: 1,
83220
- offset: 0
83221
- },
83222
- dBm: {
83223
- dimension: "signal-level",
83224
- factor: 1,
83225
- offset: 0
83226
- },
83227
- mired: {
83228
- dimension: "colour-temperature",
83229
- factor: 1,
83230
- offset: 0
83231
- },
83232
- kbps: {
83233
- dimension: "bitrate",
83234
- factor: 1,
83235
- offset: 0
83236
- },
83237
- Mbps: {
83238
- dimension: "bitrate",
83239
- factor: 1e3,
83240
- offset: 0
83241
- },
83242
- fps: {
83243
- dimension: "rate",
83244
- factor: 1,
83245
- offset: 0
83246
- }
83247
- };
83248
- //#endregion
83249
- //#region src/units/convert.ts
83250
- /**
83251
- * Pure unit-conversion API over `UNIT_TABLE`. No dependencies, no side effects.
83252
- *
83253
- * Conversion is refused (throws `UnitConversionError`, or returns `undefined`
83254
- * from the `try*` variant) whenever a unit is unknown or the two units belong
83255
- * to different dimensions — cross-dimension conversion is deliberately
83256
- * impossible so a render override can never make the value and its unit label
83257
- * disagree.
83258
- *
83259
- * Callers pass units through `normalizeUnit` (`../device/source-info.ts`) BEFORE
83260
- * calling here — the table keys only canonical spellings.
83261
- */
83262
- /** Thrown by `convertUnit` when a conversion is refused. Carries the offending
83263
- * units + a machine-readable `reason` for callers that branch on the cause. */
83264
- var UnitConversionError = class extends Error {
83265
- from;
83266
- to;
83267
- reason;
83268
- constructor(from, to, reason) {
83269
- super(`[units] cannot convert '${from}' → '${to}': ${reason}`);
83270
- this.name = "UnitConversionError";
83271
- this.from = from;
83272
- this.to = to;
83273
- this.reason = reason;
83274
- }
83275
- };
83276
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
83277
- function specFor(unit) {
83278
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
83279
- }
83280
- /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
83281
- function unitDimension(unit) {
83282
- return specFor(unit)?.dimension;
83283
- }
83284
- /** Value in `spec`'s unit → its dimension's canonical unit. */
83285
- function toCanonical(value, spec) {
83286
- return value * spec.factor + spec.offset;
83287
- }
83288
- /** Canonical-unit value → the unit described by `spec`. */
83289
- function fromCanonical(canonical, spec) {
83290
- return (canonical - spec.offset) / spec.factor;
83291
- }
83292
- /**
83293
- * Convert `value` from unit `from` to unit `to`. Identity when `from === to`.
83294
- * Throws `UnitConversionError` when either unit is unknown or the units belong
83295
- * to different dimensions.
83296
- */
83297
- function convertUnit(value, from, to) {
83298
- if (from === to) return value;
83299
- const fromSpec = specFor(from);
83300
- if (!fromSpec) throw new UnitConversionError(from, to, "unknown-from");
83301
- const toSpec = specFor(to);
83302
- if (!toSpec) throw new UnitConversionError(from, to, "unknown-to");
83303
- if (fromSpec.dimension !== toSpec.dimension) throw new UnitConversionError(from, to, "cross-dimension");
83304
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
83305
- }
83306
- /**
83307
- * Graceful variant for the render path: returns the converted value, or
83308
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
83309
- * Never throws — a render caller falls back to the raw value + source unit.
83310
- */
83311
- function tryConvertUnit(value, from, to) {
83312
- if (from === to) return value;
83313
- const fromSpec = specFor(from);
83314
- if (!fromSpec) return void 0;
83315
- const toSpec = specFor(to);
83316
- if (!toSpec) return void 0;
83317
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
83318
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
83319
- }
83320
- /**
83321
- * Whether `value` can be converted between `from` and `to` (same dimension,
83322
- * both known). Cheap boolean probe for the UI's same-dimension unit picker and
83323
- * the resolver's convertibility branch — compute ONCE so a unit label and its
83324
- * value math can never disagree.
83325
- */
83326
- function canConvertUnit(from, to) {
83327
- if (from === to) return true;
83328
- const fromDim = unitDimension(from);
83329
- if (fromDim === void 0) return false;
83330
- return fromDim === unitDimension(to);
83331
- }
83332
- /**
83333
- * Every known unit spelling in a dimension (table insertion order — canonical
83334
- * unit first). Powers the UI's same-dimension unit picker.
83335
- */
83336
- /**
83337
- * Convert a difference, not a reading. Affine units (°F) shift an absolute
83338
- * value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
83339
- */
83340
- function tryConvertDelta(value, from, to) {
83341
- if (from === to) return value;
83342
- const origin = tryConvertUnit(0, from, to);
83343
- const shifted = tryConvertUnit(value, from, to);
83344
- if (origin === void 0 || shifted === void 0) return void 0;
83345
- return shifted - origin;
83346
- }
83347
- function unitsForDimension(dimension) {
83348
- const out = [];
83349
- for (const [unit, spec] of Object.entries(UNIT_TABLE)) if (spec.dimension === dimension) out.push(unit);
83350
- return out;
83351
- }
83352
- //#endregion
83353
83696
  //#region src/composition/evaluate-field.ts
83354
83697
  /**
83355
83698
  * One composed field, evaluated from what its sources say NOW (D659).
@@ -101058,7 +101401,8 @@ var APPLIANCE_CAP_VOCABULARY = {
101058
101401
  min: 4,
101059
101402
  max: 200,
101060
101403
  step: 1,
101061
- unit: "g"
101404
+ unit: "g",
101405
+ quantity: "mass"
101062
101406
  },
101063
101407
  optional: true
101064
101408
  }]
@@ -101077,7 +101421,8 @@ var APPLIANCE_CAP_VOCABULARY = {
101077
101421
  min: 4,
101078
101422
  max: 200,
101079
101423
  step: 1,
101080
- unit: "g"
101424
+ unit: "g",
101425
+ quantity: "mass"
101081
101426
  },
101082
101427
  optional: true
101083
101428
  }, {
@@ -101088,7 +101433,8 @@ var APPLIANCE_CAP_VOCABULARY = {
101088
101433
  min: 4,
101089
101434
  max: 200,
101090
101435
  step: 1,
101091
- unit: "g"
101436
+ unit: "g",
101437
+ quantity: "mass"
101092
101438
  },
101093
101439
  optional: true
101094
101440
  }]
@@ -123395,6 +123741,8 @@ exports.PtzPresetFunctionSchema = PtzPresetFunctionSchema;
123395
123741
  exports.PtzPresetSchema = PtzPresetSchema;
123396
123742
  exports.PtzStatusSchema = PtzStatusSchema;
123397
123743
  exports.QUANTITY_BASE = require_sleep.QUANTITY_BASE;
123744
+ exports.QuantityUnitError = QuantityUnitError;
123745
+ exports.QuantityValueSchema = QuantityValueSchema;
123398
123746
  exports.QueryFilterSchema = QueryFilterSchema;
123399
123747
  exports.RATE_CONTROL_RELAXED = RATE_CONTROL_RELAXED;
123400
123748
  exports.RATE_CONTROL_TIGHT = RATE_CONTROL_TIGHT;
@@ -123937,6 +124285,7 @@ exports.UninstallNodeOutcomeSchema = UninstallNodeOutcomeSchema;
123937
124285
  exports.UninstallNodeResultSchema = UninstallNodeResultSchema;
123938
124286
  exports.UninstallPackageResultSchema = UninstallPackageResultSchema;
123939
124287
  exports.UnitConversionError = UnitConversionError;
124288
+ exports.UnitWarningSchema = UnitWarningSchema;
123940
124289
  exports.UnknownRecordKindError = require_sleep.UnknownRecordKindError;
123941
124290
  exports.UnreleasedClaimSchema = UnreleasedClaimSchema;
123942
124291
  exports.UnstampedEventMediaCountSchema = UnstampedEventMediaCountSchema;
@@ -124100,6 +124449,7 @@ exports.autoAssignProfiles = autoAssignProfiles;
124100
124449
  exports.automationControlCapability = automationControlCapability;
124101
124450
  exports.backupCapability = backupCapability;
124102
124451
  exports.bareAddonId = bareAddonId;
124452
+ exports.baseUnitForQuantity = baseUnitForQuantity;
124103
124453
  exports.baseUnitOf = require_sleep.baseUnitOf;
124104
124454
  exports.batteryCapability = batteryCapability;
124105
124455
  exports.bestLocationMatch = bestLocationMatch;
@@ -124148,6 +124498,7 @@ exports.canConvertUnit = canConvertUnit;
124148
124498
  exports.canonicalEgressPlan = canonicalEgressPlan;
124149
124499
  exports.canonicalIconKey = canonicalIconKey;
124150
124500
  exports.canonicalizeAppCallback = canonicalizeAppCallback;
124501
+ exports.capUnitResolver = capUnitResolver;
124151
124502
  exports.capabilityScopesOf = capabilityScopesOf;
124152
124503
  exports.capacityForModel = capacityForModel;
124153
124504
  exports.capacitySessionsOf = capacitySessionsOf;
@@ -124230,6 +124581,7 @@ exports.contactCapability = contactCapability;
124230
124581
  exports.containingParentIndex = containingParentIndex;
124231
124582
  exports.controlCapability = controlCapability;
124232
124583
  exports.convertPresetFeature = convertPresetFeature;
124584
+ exports.convertQuantityInput = convertQuantityInput;
124233
124585
  exports.convertUnit = convertUnit;
124234
124586
  exports.convertUnitTargets = convertUnitTargets;
124235
124587
  exports.coralUsableUnits = coralUsableUnits;
@@ -124545,6 +124897,7 @@ exports.isObjectInput = isObjectInput;
124545
124897
  exports.isOccupancyRule = isOccupancyRule;
124546
124898
  exports.isOwnerOnlyEventCategory = require_event_category.isOwnerOnlyEventCategory;
124547
124899
  exports.isPlugApplianceType = isPlugApplianceType;
124900
+ exports.isQuantityValue = isQuantityValue;
124548
124901
  exports.isReadingQuantity = require_sleep.isReadingQuantity;
124549
124902
  exports.isRemoteCustomKey = isRemoteCustomKey;
124550
124903
  exports.isRemoteKnownRole = isRemoteKnownRole;
@@ -124675,12 +125028,15 @@ exports.nodePickerItems = nodePickerItems;
124675
125028
  exports.nodePin = require_sleep.nodePin;
124676
125029
  exports.nodeRemovalHookCapability = nodeRemovalHookCapability;
124677
125030
  exports.nodesCapability = nodesCapability;
125031
+ exports.normalizeActionUnits = normalizeActionUnits;
124678
125032
  exports.normalizeAddonInitResult = require_sleep.normalizeAddonInitResult;
124679
125033
  exports.normalizeAudioLabel = normalizeAudioLabel;
125034
+ exports.normalizeConditionUnits = normalizeConditionUnits;
124680
125035
  exports.normalizeCoreBlockWrite = normalizeCoreBlockWrite;
124681
125036
  exports.normalizeNcMediaPolicy = normalizeNcMediaPolicy;
124682
125037
  exports.normalizeNodeId = require_sleep.normalizeNodeId;
124683
125038
  exports.normalizeOrigin = normalizeOrigin;
125039
+ exports.normalizeRecipeUnits = normalizeRecipeUnits;
124684
125040
  exports.normalizeSecretOwner = require_sleep.normalizeSecretOwner;
124685
125041
  exports.normalizeTokenScopes = normalizeTokenScopes;
124686
125042
  exports.normalizeUnit = normalizeUnit;
@@ -124712,6 +125068,8 @@ exports.paletteFromStored = paletteFromStored;
124712
125068
  exports.paletteLegend = paletteLegend;
124713
125069
  exports.paletteResultSchema = paletteResultSchema;
124714
125070
  exports.paletteSwatchSchema = paletteSwatchSchema;
125071
+ exports.paramMeta = paramMeta;
125072
+ exports.paramMetaOf = paramMetaOf;
124715
125073
  exports.parseAcceleratorFactsJson = parseAcceleratorFactsJson;
124716
125074
  exports.parseAdminBase = parseAdminBase;
124717
125075
  exports.parseAlertEpisodePayload = parseAlertEpisodePayload;
@@ -125097,6 +125455,7 @@ exports.turnProviderCapability = turnProviderCapability;
125097
125455
  exports.unitCompanionField = unitCompanionField;
125098
125456
  exports.unitDimension = unitDimension;
125099
125457
  exports.unitsForDimension = unitsForDimension;
125458
+ exports.unitsForQuantity = unitsForQuantity;
125100
125459
  exports.unreachableExtensionSlotRow = unreachableExtensionSlotRow;
125101
125460
  exports.unwrapSchema = unwrapSchema;
125102
125461
  exports.updateCapability = updateCapability;