@camstack/ui-library 1.2.325 → 1.2.327

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.
@@ -4,7 +4,5 @@ import type { ControlPanelProps } from "./types.js";
4
4
  export declare function toDateInput(value: string): string;
5
5
  /** Trim an HA time value ('HH:MM:SS') to the `<input type=time>` shape ('HH:MM'). */
6
6
  export declare function toTimeInput(value: string): string;
7
- /** Trim an HA datetime value to the `<input type=datetime-local>` shape
8
- * ('YYYY-MM-DDTHH:MM'). Accepts both 'T'- and space-separated forms. */
9
7
  export declare function toDateTimeInput(value: string): string;
10
8
  export declare function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic }: ControlPanelProps): ReactNode;
@@ -20,7 +20,7 @@
20
20
  * at the call site; narrowing occurs after `useDeviceStateSlice`.
21
21
  */
22
22
  import type { ReactNode } from "react";
23
- import type { DeviceProxy, SliceHandle } from "@camstack/types";
23
+ import type { DeviceProxy, ReadingQuantity, SliceHandle } from "@camstack/types";
24
24
  import { type UseDeviceProxyTrpc } from "../../hooks/use-device-proxy";
25
25
  import { type ControlDateTimeFormat } from "../../lib/format-control-datetime";
26
26
  import type { DeviceItemDevice } from "../device-item/helpers";
@@ -28,12 +28,8 @@ interface NumericDescriptor {
28
28
  readonly kind: "numeric";
29
29
  /** Field in the slice that holds the numeric value. */
30
30
  readonly field: string;
31
- /** Canonical display unit for TYPED roles (temperature → '°C', humidity → '%',
32
- * ambient-light → 'lx', pressure → 'hPa'). When present the UI renders this
33
- * directly WITHOUT consulting the slice or sourceInfo.
34
- * For the generic `numeric-sensor` role this is `undefined` — the unit is
35
- * read live from the slice (`slice.unit`). */
36
- readonly canonicalUnit?: string;
31
+ /** Quantity of the stored number. `device` reads `sourceInfo.quantity`. */
32
+ readonly quantity?: ReadingQuantity;
37
33
  /** Accessor into DeviceProxy['state'] — returns a typed slice handle. */
38
34
  readonly getSliceHandle: (state: DeviceProxy["state"]) => SliceHandle<unknown>;
39
35
  }
@@ -99,13 +95,9 @@ export declare function hasBatteryNumericValue(slice: unknown, field: string): b
99
95
  * avoids dynamic string indexing — each one is a concrete property
100
96
  * access on the typed `DeviceProxy['state']` shape.
101
97
  *
102
- * Canonical unit rules:
103
- * - Typed roles (temperature/humidity/ambient-light/pressure/power/energy)
104
- * carry a `canonicalUnit` constant. The slice is normalised to one unit
105
- * so no per-device lookup is needed.
106
- * - Generic `numeric-sensor` has no `canonicalUnit`. The unit is read live
107
- * from the slice (`slice.unit`) as populated by the provider from the
108
- * upstream source on every state push.
98
+ * The unit comes from the descriptor's quantity through `resolveSensorDisplay`.
99
+ * A generic numeric sensor uses quantity `device` and the device's
100
+ * `sourceInfo.quantity`.
109
101
  */
110
102
  export declare const ROLE_DESCRIPTOR: Record<string, SensorDescriptor>;
111
103
  export interface StringSliceValue {
@@ -841,6 +841,8 @@ export declare const useFeatureProbeGetStatus: typeof trpc.featureProbe.getStatu
841
841
  export declare const useFloodGetStatus: typeof trpc.flood.getStatus.useQuery;
842
842
  /** Generated alias around `trpc.gas.getStatus.useQuery`. */
843
843
  export declare const useGasGetStatus: typeof trpc.gas.getStatus.useQuery;
844
+ /** Generated alias around `trpc.groupStatus.getStatus.useQuery`. */
845
+ export declare const useGroupStatusGetStatus: typeof trpc.groupStatus.getStatus.useQuery;
844
846
  /** Generated alias around `trpc.heatingValve.getStatus.useQuery`. */
845
847
  export declare const useHeatingValveGetStatus: typeof trpc.heatingValve.getStatus.useQuery;
846
848
  /** Generated alias around `trpc.humidifier.setOn.useMutation`. */
package/dist/index.cjs CHANGED
@@ -11121,6 +11121,8 @@ var useFeatureProbeGetStatus = trpc.featureProbe.getStatus.useQuery;
11121
11121
  var useFloodGetStatus = trpc.flood.getStatus.useQuery;
11122
11122
  /** Generated alias around `trpc.gas.getStatus.useQuery`. */
11123
11123
  var useGasGetStatus = trpc.gas.getStatus.useQuery;
11124
+ /** Generated alias around `trpc.groupStatus.getStatus.useQuery`. */
11125
+ var useGroupStatusGetStatus = trpc.groupStatus.getStatus.useQuery;
11124
11126
  /** Generated alias around `trpc.heatingValve.getStatus.useQuery`. */
11125
11127
  var useHeatingValveGetStatus = trpc.heatingValve.getStatus.useQuery;
11126
11128
  /** Generated alias around `trpc.humidifier.setOn.useMutation`. */
@@ -20973,21 +20975,16 @@ Skeleton.displayName = "Skeleton";
20973
20975
  /** `value` null = unknown (D393) — renders an em-dash, never a fake `0`. */
20974
20976
  function formatRemaining(t, r) {
20975
20977
  if (r.value === null) return "—";
20976
- const count = r.value;
20977
- switch (r.unit) {
20978
- case "days": return t("uiLibrary.consumablesPanel.days", {
20979
- one: "{{count}} day",
20980
- other: "{{count}} days"
20981
- }, { count });
20982
- case "hours": return t("uiLibrary.consumablesPanel.hours", {
20983
- one: "{{count}} hour",
20984
- other: "{{count}} hours"
20985
- }, { count });
20986
- case "cycles": return t("uiLibrary.consumablesPanel.cycles", {
20987
- one: "{{count}} cycle",
20988
- other: "{{count}} cycles"
20989
- }, { count });
20990
- }
20978
+ if (r.kind === "count") return t("uiLibrary.consumablesPanel.cycles", {
20979
+ one: "{{count}} cycle",
20980
+ other: "{{count}} cycles"
20981
+ }, { count: r.value });
20982
+ const rendered = (0, _camstack_types.formatReading)({
20983
+ value: r.value,
20984
+ quantity: "duration",
20985
+ precision: 0
20986
+ });
20987
+ return rendered.unit.length === 0 ? rendered.text : `${rendered.text} ${rendered.unit}`;
20991
20988
  }
20992
20989
  /** Format a ms-epoch as a compact local date for the "replaced" hint. */
20993
20990
  function formatResetAt(ms) {
@@ -46083,6 +46080,66 @@ function formatNumeric(value, precision) {
46083
46080
  return Number.isInteger(value) ? String(value) : value.toFixed(1);
46084
46081
  }
46085
46082
  //#endregion
46083
+ //#region src/lib/resolve-sensor-display.ts
46084
+ function canonUnit(raw) {
46085
+ if (raw === void 0) return void 0;
46086
+ return (0, _camstack_types.normalizeUnit)(raw) ?? raw;
46087
+ }
46088
+ function concreteQuantity(input) {
46089
+ if (input.quantity === void 0) return void 0;
46090
+ if (input.quantity === "device") {
46091
+ const chosen = input.deviceQuantity;
46092
+ if (chosen === void 0 || chosen === "device") return void 0;
46093
+ return chosen;
46094
+ }
46095
+ return input.quantity;
46096
+ }
46097
+ function resolveSensorDisplay(input) {
46098
+ const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
46099
+ const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.declarationPrecision;
46100
+ const quantity = concreteQuantity(input);
46101
+ const preference = input.preference ?? _camstack_types.DEFAULT_DISPLAY_PREFERENCE;
46102
+ const base = quantity === void 0 ? void 0 : (0, _camstack_types.baseUnitOf)(quantity);
46103
+ const preferred = quantity === void 0 ? void 0 : (0, _camstack_types.displayUnitFor)(quantity, preference);
46104
+ const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
46105
+ const overrideOk = overrideUnit !== void 0 && base !== void 0 && overrideUnit !== base && (0, _camstack_types.canConvertUnit)(base, overrideUnit);
46106
+ const unit = overrideOk ? overrideUnit : preferred ?? "";
46107
+ const scales = quantity === "duration" || quantity === "information" || quantity === "duration-ms";
46108
+ const toDisplayValue = (raw) => {
46109
+ if (scales || base === void 0 || unit.length === 0 || unit === base) return raw;
46110
+ return (0, _camstack_types.tryConvertUnit)(raw, base, unit) ?? raw;
46111
+ };
46112
+ const fromDisplayValue = (shown) => {
46113
+ if (scales || base === void 0 || unit.length === 0 || unit === base) return shown;
46114
+ return (0, _camstack_types.tryConvertUnit)(shown, unit, base) ?? shown;
46115
+ };
46116
+ const format = (raw) => {
46117
+ if (quantity !== void 0 && !overrideOk) {
46118
+ const rendered = (0, _camstack_types.formatReading)({
46119
+ value: raw,
46120
+ quantity,
46121
+ preference,
46122
+ precision
46123
+ });
46124
+ return {
46125
+ text: rendered.text,
46126
+ unit: rendered.unit
46127
+ };
46128
+ }
46129
+ return {
46130
+ text: formatNumeric(toDisplayValue(raw), precision),
46131
+ unit
46132
+ };
46133
+ };
46134
+ return {
46135
+ unit,
46136
+ precision,
46137
+ toDisplayValue,
46138
+ fromDisplayValue,
46139
+ format
46140
+ };
46141
+ }
46142
+ //#endregion
46086
46143
  //#region src/lib/format-control-datetime.ts
46087
46144
  var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})/;
46088
46145
  var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
@@ -46148,8 +46205,20 @@ function toDateInput(value) {
46148
46205
  function toTimeInput(value) {
46149
46206
  return value.slice(0, 5);
46150
46207
  }
46151
- /** Trim an HA datetime value to the `<input type=datetime-local>` shape
46152
- * ('YYYY-MM-DDTHH:MM'). Accepts both 'T'- and space-separated forms. */
46208
+ function datetimeEditor(format, rawValue) {
46209
+ if (format === "date") return {
46210
+ type: "date",
46211
+ value: toDateInput(rawValue)
46212
+ };
46213
+ if (format === "time") return {
46214
+ type: "time",
46215
+ value: toTimeInput(rawValue)
46216
+ };
46217
+ return {
46218
+ type: "datetime-local",
46219
+ value: toDateTimeInput(rawValue)
46220
+ };
46221
+ }
46153
46222
  function toDateTimeInput(value) {
46154
46223
  return (value.includes("T") ? value : value.replace(" ", "T")).slice(0, 16);
46155
46224
  }
@@ -46242,24 +46311,35 @@ function TextControl({ value, busy, onCommit }) {
46242
46311
  className: cn("rounded border border-border bg-surface px-2 py-0.5 text-xs", busy && "opacity-40 cursor-not-allowed")
46243
46312
  });
46244
46313
  }
46245
- function ControlWidget({ slice, busy, setValueFn, layout }) {
46314
+ function ControlWidget({ slice, busy, setValueFn, layout, quantity, display, dateTimeShape }) {
46246
46315
  const t = useUiText();
46247
46316
  switch (slice.kind) {
46248
- case "numeric": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumericControl, {
46249
- value: typeof slice.value === "number" ? slice.value : Number(slice.value),
46250
- busy,
46251
- onCommit: (v) => {
46252
- setValueFn({ control: {
46253
- kind: "numeric",
46254
- value: v
46255
- } });
46256
- },
46257
- unit: slice.unit,
46258
- min: slice.min,
46259
- max: slice.max,
46260
- step: slice.step,
46261
- precision: slice.precision
46262
- });
46317
+ case "numeric": {
46318
+ const numericValue = typeof slice.value === "number" ? slice.value : Number(slice.value);
46319
+ const resolved = quantity === void 0 ? void 0 : resolveSensorDisplay({
46320
+ quantity,
46321
+ display,
46322
+ capName: "control"
46323
+ });
46324
+ const toShown = resolved?.toDisplayValue ?? ((raw) => raw);
46325
+ const toBase = resolved?.fromDisplayValue ?? ((shown) => shown);
46326
+ const unit = resolved !== void 0 && resolved.unit.length > 0 ? resolved.unit : void 0;
46327
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumericControl, {
46328
+ value: toShown(numericValue),
46329
+ busy,
46330
+ onCommit: (v) => {
46331
+ setValueFn({ control: {
46332
+ kind: "numeric",
46333
+ value: toBase(v)
46334
+ } });
46335
+ },
46336
+ unit,
46337
+ min: slice.min === void 0 ? void 0 : toShown(slice.min),
46338
+ max: slice.max === void 0 ? void 0 : toShown(slice.max),
46339
+ step: slice.step === void 0 ? void 0 : toShown(slice.step),
46340
+ precision: resolved?.precision
46341
+ });
46342
+ }
46263
46343
  case "select": {
46264
46344
  const selectValue = String(slice.value);
46265
46345
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
@@ -46292,7 +46372,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
46292
46372
  });
46293
46373
  case "datetime": {
46294
46374
  const rawValue = String(slice.value);
46295
- const format = slice.format ?? "datetime";
46375
+ const format = dateTimeShape ?? "datetime";
46296
46376
  if (layout === "compact") {
46297
46377
  if (rawValue === "") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
46298
46378
  "aria-label": t("uiLibrary.controlPanel.value", "Control value"),
@@ -46305,8 +46385,9 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
46305
46385
  children: formatControlDateTime(rawValue, format)
46306
46386
  });
46307
46387
  }
46308
- const inputType = format === "date" ? "date" : format === "time" ? "time" : "datetime-local";
46309
- const inputValue = format === "date" ? toDateInput(rawValue) : format === "time" ? toTimeInput(rawValue) : toDateTimeInput(rawValue);
46388
+ const editor = datetimeEditor(format, rawValue);
46389
+ const inputType = editor.type;
46390
+ const inputValue = editor.value;
46310
46391
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
46311
46392
  type: inputType,
46312
46393
  "aria-label": t("uiLibrary.controlPanel.value", "Control value"),
@@ -46326,6 +46407,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
46326
46407
  }
46327
46408
  }
46328
46409
  function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic }) {
46410
+ const info = useOptionalSystem()?.getDeviceInfo(deviceId) ?? null;
46329
46411
  const { cap, slice, available } = useDeviceCapability({
46330
46412
  trpc,
46331
46413
  deviceId,
@@ -46335,6 +46417,8 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
46335
46417
  const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
46336
46418
  if (display === void 0) return null;
46337
46419
  const busy = !available;
46420
+ const chosen = info?.sourceInfo?.quantity;
46421
+ const quantity = (0, _camstack_types.isReadingQuantity)(chosen) && chosen !== "device" && chosen !== "none" ? chosen : void 0;
46338
46422
  const setValueFn = (input) => {
46339
46423
  if (!busy && cap) {
46340
46424
  applyOptimistic({ value: input.control.value });
@@ -46348,7 +46432,10 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
46348
46432
  slice: display,
46349
46433
  busy,
46350
46434
  setValueFn,
46351
- layout
46435
+ layout,
46436
+ quantity,
46437
+ display: info?.display,
46438
+ dateTimeShape: info?.sourceInfo?.dateTimeShape
46352
46439
  })
46353
46440
  });
46354
46441
  }
@@ -46452,30 +46539,6 @@ function PopoverRowAction({ icon: Icon, title, children }) {
46452
46539
  }), document.body)] });
46453
46540
  }
46454
46541
  //#endregion
46455
- //#region src/lib/resolve-sensor-display.ts
46456
- /** Table keys are canonical spellings; providers often send `C` / `F` / `celsius`. */
46457
- function canonUnit(raw) {
46458
- if (raw === void 0) return void 0;
46459
- return (0, _camstack_types.normalizeUnit)(raw) ?? raw;
46460
- }
46461
- var identity = (raw) => raw;
46462
- function resolveSensorDisplay(input) {
46463
- const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
46464
- const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.slicePrecision;
46465
- const sourceUnit = canonUnit(input.sliceUnit ?? input.canonicalUnit);
46466
- const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
46467
- if (overrideUnit !== void 0 && sourceUnit !== void 0 && overrideUnit !== sourceUnit && (0, _camstack_types.canConvertUnit)(sourceUnit, overrideUnit)) return {
46468
- unit: overrideUnit,
46469
- precision,
46470
- toDisplayValue: (raw) => (0, _camstack_types.tryConvertUnit)(raw, sourceUnit, overrideUnit) ?? raw
46471
- };
46472
- return {
46473
- unit: sourceUnit ?? "",
46474
- precision,
46475
- toDisplayValue: identity
46476
- };
46477
- }
46478
- //#endregion
46479
46542
  //#region src/composites/device-controls/sensor-value-atom.tsx
46480
46543
  /**
46481
46544
  * Binary fallback for the `battery-sensor` role.
@@ -46516,102 +46579,105 @@ function hasBatteryNumericValue(slice, field) {
46516
46579
  * avoids dynamic string indexing — each one is a concrete property
46517
46580
  * access on the typed `DeviceProxy['state']` shape.
46518
46581
  *
46519
- * Canonical unit rules:
46520
- * - Typed roles (temperature/humidity/ambient-light/pressure/power/energy)
46521
- * carry a `canonicalUnit` constant. The slice is normalised to one unit
46522
- * so no per-device lookup is needed.
46523
- * - Generic `numeric-sensor` has no `canonicalUnit`. The unit is read live
46524
- * from the slice (`slice.unit`) as populated by the provider from the
46525
- * upstream source on every state push.
46582
+ * The unit comes from the descriptor's quantity through `resolveSensorDisplay`.
46583
+ * A generic numeric sensor uses quantity `device` and the device's
46584
+ * `sourceInfo.quantity`.
46526
46585
  */
46527
46586
  var ROLE_DESCRIPTOR = {
46528
46587
  "temperature-sensor": {
46529
46588
  kind: "numeric",
46530
46589
  field: "celsius",
46531
- canonicalUnit: "°C",
46590
+ quantity: "temperature",
46532
46591
  getSliceHandle: (s) => s.temperatureSensor
46533
46592
  },
46534
46593
  "humidity-sensor": {
46535
46594
  kind: "numeric",
46536
46595
  field: "percent",
46537
- canonicalUnit: "%",
46596
+ quantity: "humidity",
46538
46597
  getSliceHandle: (s) => s.humiditySensor
46539
46598
  },
46540
46599
  "ambient-light-sensor": {
46541
46600
  kind: "numeric",
46542
46601
  field: "lux",
46543
- canonicalUnit: "lx",
46602
+ quantity: "illuminance",
46544
46603
  getSliceHandle: (s) => s.ambientLightSensor
46545
46604
  },
46546
46605
  "pressure-sensor": {
46547
46606
  kind: "numeric",
46548
46607
  field: "hpa",
46549
- canonicalUnit: "hPa",
46608
+ quantity: "pressure",
46550
46609
  getSliceHandle: (s) => s.pressureSensor
46551
46610
  },
46552
46611
  "numeric-sensor": {
46553
46612
  kind: "numeric",
46554
46613
  field: "value",
46614
+ quantity: "device",
46555
46615
  getSliceHandle: (s) => s.numericSensor
46556
46616
  },
46557
46617
  "wind-sensor": {
46558
46618
  kind: "numeric",
46559
46619
  field: "value",
46620
+ quantity: "device",
46560
46621
  getSliceHandle: (s) => s.numericSensor
46561
46622
  },
46562
46623
  "rain-sensor": {
46563
46624
  kind: "numeric",
46564
46625
  field: "value",
46626
+ quantity: "device",
46565
46627
  getSliceHandle: (s) => s.numericSensor
46566
46628
  },
46567
46629
  "uv-sensor": {
46568
46630
  kind: "numeric",
46569
46631
  field: "value",
46632
+ quantity: "device",
46570
46633
  getSliceHandle: (s) => s.numericSensor
46571
46634
  },
46572
46635
  "solar-radiation-sensor": {
46573
46636
  kind: "numeric",
46574
46637
  field: "value",
46638
+ quantity: "device",
46575
46639
  getSliceHandle: (s) => s.numericSensor
46576
46640
  },
46577
46641
  "soil-moisture-sensor": {
46578
46642
  kind: "numeric",
46579
46643
  field: "value",
46644
+ quantity: "device",
46580
46645
  getSliceHandle: (s) => s.numericSensor
46581
46646
  },
46582
46647
  "power-sensor": {
46583
46648
  kind: "numeric",
46584
46649
  field: "watts",
46585
- canonicalUnit: "W",
46650
+ quantity: "power",
46586
46651
  getSliceHandle: (s) => s.powerMeter
46587
46652
  },
46588
46653
  "energy-sensor": {
46589
46654
  kind: "numeric",
46590
46655
  field: "kwhTotal",
46591
- canonicalUnit: "kWh",
46656
+ quantity: "energy",
46592
46657
  getSliceHandle: (s) => s.powerMeter
46593
46658
  },
46594
46659
  "voltage-sensor": {
46595
46660
  kind: "numeric",
46596
46661
  field: "volts",
46597
- canonicalUnit: "V",
46662
+ quantity: "voltage",
46598
46663
  getSliceHandle: (s) => s.powerMeter
46599
46664
  },
46600
46665
  "current-sensor": {
46601
46666
  kind: "numeric",
46602
46667
  field: "amps",
46603
- canonicalUnit: "A",
46668
+ quantity: "current",
46604
46669
  getSliceHandle: (s) => s.powerMeter
46605
46670
  },
46606
46671
  "battery-sensor": {
46607
46672
  kind: "numeric",
46608
46673
  field: "value",
46609
- canonicalUnit: "%",
46674
+ quantity: "percentage",
46610
46675
  getSliceHandle: (s) => s.numericSensor
46611
46676
  },
46612
46677
  "generic-sensor": {
46613
46678
  kind: "numeric",
46614
46679
  field: "value",
46680
+ quantity: "device",
46615
46681
  getSliceHandle: (s) => s.numericSensor
46616
46682
  },
46617
46683
  "motion-sensor": {
@@ -46729,19 +46795,16 @@ function SensorValueAtom({ trpc, device }) {
46729
46795
  children: enumSensorSlice.value
46730
46796
  });
46731
46797
  if (numericIsColStart) return null;
46732
- const sliceRecord = slice;
46733
- const liveUnit = typeof sliceRecord["unit"] === "string" ? sliceRecord["unit"] : void 0;
46734
- const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
46735
- const resolved = resolveSensorDisplay({
46798
+ const deviceQuantity = device.sourceInfo?.quantity;
46799
+ const rendered = resolveSensorDisplay({
46736
46800
  display: device.display,
46737
46801
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
46738
46802
  roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : void 0,
46739
- sliceUnit: liveUnit,
46740
- slicePrecision,
46741
- canonicalUnit: descriptor.canonicalUnit
46742
- });
46743
- const unit = resolved.unit;
46744
- const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
46803
+ quantity: descriptor.quantity ?? "device",
46804
+ deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
46805
+ }).format(rawValue);
46806
+ const unit = rendered.unit;
46807
+ const formatted = rendered.text;
46745
46808
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
46746
46809
  className: cn("inline-flex items-baseline gap-0.5 tabular-nums text-xs text-foreground"),
46747
46810
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatted }), unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -46771,7 +46834,7 @@ function SensorValueAtom({ trpc, device }) {
46771
46834
  if (descriptor.kind === "datetime") {
46772
46835
  if (!isStringSliceValue(slice)) return null;
46773
46836
  if (slice.value === "") return null;
46774
- const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
46837
+ const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
46775
46838
  const formatted = formatControlDateTime(slice.value, format);
46776
46839
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
46777
46840
  className: cn("inline-flex items-baseline text-xs text-foreground"),
@@ -47326,17 +47389,14 @@ function SensorInlineControl({ trpc, deviceId }) {
47326
47389
  *
47327
47390
  * Renders:
47328
47391
  * - Typed numeric sensors: value in `text-3xl font-semibold tabular-nums` +
47329
- * canonical unit from ROLE_DESCRIPTOR (e.g. temperature-sensor → '°C').
47330
- * - Generic numeric-sensor: value + unit read live from the slice
47331
- * (`slice.unit`, populated by the HA provider from state attributes).
47392
+ * the unit `resolveSensorDisplay` derives from the quantity.
47393
+ * - Generic numeric-sensor: the same formatter, with the device's quantity.
47332
47394
  * - Binary sensors: coloured state badge ("Detected"/"Clear" etc.) — same
47333
47395
  * labels and colours as SensorValueAtom's binary branch.
47334
47396
  * - Presence: Home/Away/zone-name badge — same labels as SensorValueAtom.
47335
47397
  * - All: sensor name label below the value.
47336
47398
  *
47337
- * Unit resolution uses the same logic as SensorValueAtom (live slice.unit
47338
- * wins, ROLE_DESCRIPTOR canonicalUnit as fallback) — the two surfaces stay
47339
- * in sync.
47399
+ * Unit resolution uses the same formatter as SensorValueAtom.
47340
47400
  */
47341
47401
  function isNumericSliceValue(v) {
47342
47402
  return typeof v === "object" && v !== null;
@@ -47403,19 +47463,16 @@ function SensorHeroInner({ trpc, device }) {
47403
47463
  if (!isNumericSliceValue(slice)) return null;
47404
47464
  const rawValue = slice[descriptor.field];
47405
47465
  if (typeof rawValue !== "number") return null;
47406
- const sliceRecord = slice;
47407
- const liveUnit = typeof sliceRecord["unit"] === "string" ? sliceRecord["unit"] : void 0;
47408
- const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
47409
- const resolved = resolveSensorDisplay({
47466
+ const deviceQuantity = device.sourceInfo?.quantity;
47467
+ const rendered = resolveSensorDisplay({
47410
47468
  display: device.display,
47411
47469
  capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
47412
47470
  roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : roleDefaultsData?.defaults["temperature-sensor"],
47413
- sliceUnit: liveUnit,
47414
- slicePrecision,
47415
- canonicalUnit: descriptor.canonicalUnit
47416
- });
47417
- const unit = resolved.unit;
47418
- const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
47471
+ quantity: descriptor.quantity ?? "device",
47472
+ deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
47473
+ }).format(rawValue);
47474
+ const unit = rendered.unit;
47475
+ const formatted = rendered.text;
47419
47476
  const occupancyOn = occupancySlice !== void 0 && isBinarySliceValue(occupancySlice) && typeof occupancySlice["on"] === "boolean" ? Boolean(occupancySlice["on"]) : null;
47420
47477
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47421
47478
  className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
@@ -47471,7 +47528,7 @@ function SensorHeroInner({ trpc, device }) {
47471
47528
  children: t("uiLibrary.sensorHeroCard.awaiting", "Awaiting state…")
47472
47529
  })]
47473
47530
  });
47474
- const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
47531
+ const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
47475
47532
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
47476
47533
  className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
47477
47534
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -49927,10 +49984,15 @@ function useWeather(trpc, deviceId) {
49927
49984
  slice: (d) => d.state.weather
49928
49985
  });
49929
49986
  }
49930
- /** Format a temperature + unit readout (e.g. `21°C`). */
49931
- function formatTemp(temp, unit) {
49932
- if (temp === null) return "—";
49933
- return `${Math.round(temp)}${unit ?? "°"}`;
49987
+ /** Format a base-unit weather reading (metric). */
49988
+ function formatWeather(value, quantity) {
49989
+ if (value === null) return "—";
49990
+ const rendered = (0, _camstack_types.formatReading)({
49991
+ value,
49992
+ quantity,
49993
+ precision: 0
49994
+ });
49995
+ return rendered.unit.length === 0 ? rendered.text : `${rendered.text}${rendered.unit}`;
49934
49996
  }
49935
49997
  /**
49936
49998
  * WeatherInlineControl — compact condition icon + temperature readout
@@ -49950,7 +50012,7 @@ function WeatherInlineControl({ trpc, deviceId }) {
49950
50012
  children: inlineGlyph(meta.glyph)
49951
50013
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
49952
50014
  className: "tabular-nums",
49953
- children: formatTemp(slice.temperature, slice.temperatureUnit)
50015
+ children: formatWeather(slice.temperature, "temperature")
49954
50016
  })]
49955
50017
  });
49956
50018
  }
@@ -50260,7 +50322,11 @@ function WeatherHeroCard({ trpc, deviceId }) {
50260
50322
  if (slice === void 0) return null;
50261
50323
  const meta = weatherConditionMeta(slice.condition, t);
50262
50324
  const tint = weatherTint(slice.condition);
50263
- const unit = slice.temperatureUnit ?? "°";
50325
+ const temperature = slice.temperature === null ? void 0 : (0, _camstack_types.formatReading)({
50326
+ value: slice.temperature,
50327
+ quantity: "temperature",
50328
+ precision: 0
50329
+ });
50264
50330
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
50265
50331
  className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4",
50266
50332
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -50275,11 +50341,14 @@ function WeatherHeroCard({ trpc, deviceId }) {
50275
50341
  children: [
50276
50342
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50277
50343
  className: "text-3xl font-semibold tabular-nums text-foreground",
50278
- children: slice.temperature !== null ? Math.round(slice.temperature) : "—"
50344
+ children: temperature !== void 0 ? temperature.text : "—"
50279
50345
  }),
50280
50346
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50281
50347
  className: "text-base text-foreground-subtle",
50282
- children: unit
50348
+ children: temperature !== void 0 ? temperature.unit : (0, _camstack_types.formatReading)({
50349
+ value: 0,
50350
+ quantity: "temperature"
50351
+ }).unit
50283
50352
  }),
50284
50353
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
50285
50354
  className: "text-xs text-foreground-subtle ml-2 capitalize",
@@ -50300,12 +50369,12 @@ function WeatherHeroCard({ trpc, deviceId }) {
50300
50369
  style: { transform: `rotate(${slice.windBearing}deg)` }
50301
50370
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
50302
50371
  label: t("uiLibrary.weatherControl.wind", "Wind"),
50303
- value: `${Math.round(slice.windSpeed)} ${slice.windSpeedUnit ?? ""}`.trim()
50372
+ value: formatWeather(slice.windSpeed, "speed")
50304
50373
  }),
50305
50374
  slice.pressure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
50306
50375
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5" }),
50307
50376
  label: t("uiLibrary.weatherControl.pressure", "Pressure"),
50308
- value: `${Math.round(slice.pressure)} ${slice.pressureUnit ?? ""}`.trim()
50377
+ value: formatWeather(slice.pressure, "pressure")
50309
50378
  })
50310
50379
  ]
50311
50380
  })]
@@ -50544,6 +50613,16 @@ var VOLUME_MAX = 9;
50544
50613
  /** Amber kibble accent for the bowl fill; destructive red when low. */
50545
50614
  var KIBBLE = "#f59e0b";
50546
50615
  var KIBBLE_LOW = "#ef4444";
50616
+ function desiccantLabel(t, seconds) {
50617
+ const rendered = (0, _camstack_types.formatReading)({
50618
+ value: seconds,
50619
+ quantity: "duration"
50620
+ });
50621
+ return t("uiLibrary.petFeeder.desiccant", "{{text}}{{unit}} desiccant", {
50622
+ text: rendered.text,
50623
+ unit: rendered.unit
50624
+ });
50625
+ }
50547
50626
  function clampPortion(grams) {
50548
50627
  return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
50549
50628
  }
@@ -50666,7 +50745,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
50666
50745
  }),
50667
50746
  display.desiccantLeftDays !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
50668
50747
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
50669
- label: t("uiLibrary.petFeeder.desiccant", "{{days}}d desiccant", { days: display.desiccantLeftDays })
50748
+ label: desiccantLabel(t, display.desiccantLeftDays)
50670
50749
  }),
50671
50750
  onBatteries && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
50672
50751
  className: "inline-flex items-center gap-1 font-medium text-amber-500",
@@ -70033,6 +70112,7 @@ exports.useFanControlSetPreset = useFanControlSetPreset;
70033
70112
  exports.useFeatureProbeGetStatus = useFeatureProbeGetStatus;
70034
70113
  exports.useFloodGetStatus = useFloodGetStatus;
70035
70114
  exports.useGasGetStatus = useGasGetStatus;
70115
+ exports.useGroupStatusGetStatus = useGroupStatusGetStatus;
70036
70116
  exports.useHeatingValveGetStatus = useHeatingValveGetStatus;
70037
70117
  exports.useHistoryPanels = useHistoryPanels;
70038
70118
  exports.useHistoryReader = useHistoryReader;