@camstack/ui-library 1.2.325 → 1.2.326
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
|
-
/**
|
|
32
|
-
|
|
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
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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 {
|
package/dist/index.cjs
CHANGED
|
@@ -20973,21 +20973,16 @@ Skeleton.displayName = "Skeleton";
|
|
|
20973
20973
|
/** `value` null = unknown (D393) — renders an em-dash, never a fake `0`. */
|
|
20974
20974
|
function formatRemaining(t, r) {
|
|
20975
20975
|
if (r.value === null) return "—";
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
|
|
20979
|
-
|
|
20980
|
-
|
|
20981
|
-
|
|
20982
|
-
|
|
20983
|
-
|
|
20984
|
-
|
|
20985
|
-
|
|
20986
|
-
case "cycles": return t("uiLibrary.consumablesPanel.cycles", {
|
|
20987
|
-
one: "{{count}} cycle",
|
|
20988
|
-
other: "{{count}} cycles"
|
|
20989
|
-
}, { count });
|
|
20990
|
-
}
|
|
20976
|
+
if (r.kind === "count") return t("uiLibrary.consumablesPanel.cycles", {
|
|
20977
|
+
one: "{{count}} cycle",
|
|
20978
|
+
other: "{{count}} cycles"
|
|
20979
|
+
}, { count: r.value });
|
|
20980
|
+
const rendered = (0, _camstack_types.formatReading)({
|
|
20981
|
+
value: r.value,
|
|
20982
|
+
quantity: "duration",
|
|
20983
|
+
precision: 0
|
|
20984
|
+
});
|
|
20985
|
+
return rendered.unit.length === 0 ? rendered.text : `${rendered.text} ${rendered.unit}`;
|
|
20991
20986
|
}
|
|
20992
20987
|
/** Format a ms-epoch as a compact local date for the "replaced" hint. */
|
|
20993
20988
|
function formatResetAt(ms) {
|
|
@@ -46083,6 +46078,66 @@ function formatNumeric(value, precision) {
|
|
|
46083
46078
|
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
46084
46079
|
}
|
|
46085
46080
|
//#endregion
|
|
46081
|
+
//#region src/lib/resolve-sensor-display.ts
|
|
46082
|
+
function canonUnit(raw) {
|
|
46083
|
+
if (raw === void 0) return void 0;
|
|
46084
|
+
return (0, _camstack_types.normalizeUnit)(raw) ?? raw;
|
|
46085
|
+
}
|
|
46086
|
+
function concreteQuantity(input) {
|
|
46087
|
+
if (input.quantity === void 0) return void 0;
|
|
46088
|
+
if (input.quantity === "device") {
|
|
46089
|
+
const chosen = input.deviceQuantity;
|
|
46090
|
+
if (chosen === void 0 || chosen === "device") return void 0;
|
|
46091
|
+
return chosen;
|
|
46092
|
+
}
|
|
46093
|
+
return input.quantity;
|
|
46094
|
+
}
|
|
46095
|
+
function resolveSensorDisplay(input) {
|
|
46096
|
+
const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
|
|
46097
|
+
const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.declarationPrecision;
|
|
46098
|
+
const quantity = concreteQuantity(input);
|
|
46099
|
+
const preference = input.preference ?? _camstack_types.DEFAULT_DISPLAY_PREFERENCE;
|
|
46100
|
+
const base = quantity === void 0 ? void 0 : (0, _camstack_types.baseUnitOf)(quantity);
|
|
46101
|
+
const preferred = quantity === void 0 ? void 0 : (0, _camstack_types.displayUnitFor)(quantity, preference);
|
|
46102
|
+
const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
|
|
46103
|
+
const overrideOk = overrideUnit !== void 0 && base !== void 0 && overrideUnit !== base && (0, _camstack_types.canConvertUnit)(base, overrideUnit);
|
|
46104
|
+
const unit = overrideOk ? overrideUnit : preferred ?? "";
|
|
46105
|
+
const scales = quantity === "duration" || quantity === "information" || quantity === "duration-ms";
|
|
46106
|
+
const toDisplayValue = (raw) => {
|
|
46107
|
+
if (scales || base === void 0 || unit.length === 0 || unit === base) return raw;
|
|
46108
|
+
return (0, _camstack_types.tryConvertUnit)(raw, base, unit) ?? raw;
|
|
46109
|
+
};
|
|
46110
|
+
const fromDisplayValue = (shown) => {
|
|
46111
|
+
if (scales || base === void 0 || unit.length === 0 || unit === base) return shown;
|
|
46112
|
+
return (0, _camstack_types.tryConvertUnit)(shown, unit, base) ?? shown;
|
|
46113
|
+
};
|
|
46114
|
+
const format = (raw) => {
|
|
46115
|
+
if (quantity !== void 0 && !overrideOk) {
|
|
46116
|
+
const rendered = (0, _camstack_types.formatReading)({
|
|
46117
|
+
value: raw,
|
|
46118
|
+
quantity,
|
|
46119
|
+
preference,
|
|
46120
|
+
precision
|
|
46121
|
+
});
|
|
46122
|
+
return {
|
|
46123
|
+
text: rendered.text,
|
|
46124
|
+
unit: rendered.unit
|
|
46125
|
+
};
|
|
46126
|
+
}
|
|
46127
|
+
return {
|
|
46128
|
+
text: formatNumeric(toDisplayValue(raw), precision),
|
|
46129
|
+
unit
|
|
46130
|
+
};
|
|
46131
|
+
};
|
|
46132
|
+
return {
|
|
46133
|
+
unit,
|
|
46134
|
+
precision,
|
|
46135
|
+
toDisplayValue,
|
|
46136
|
+
fromDisplayValue,
|
|
46137
|
+
format
|
|
46138
|
+
};
|
|
46139
|
+
}
|
|
46140
|
+
//#endregion
|
|
46086
46141
|
//#region src/lib/format-control-datetime.ts
|
|
46087
46142
|
var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
46088
46143
|
var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
@@ -46148,8 +46203,20 @@ function toDateInput(value) {
|
|
|
46148
46203
|
function toTimeInput(value) {
|
|
46149
46204
|
return value.slice(0, 5);
|
|
46150
46205
|
}
|
|
46151
|
-
|
|
46152
|
-
|
|
46206
|
+
function datetimeEditor(format, rawValue) {
|
|
46207
|
+
if (format === "date") return {
|
|
46208
|
+
type: "date",
|
|
46209
|
+
value: toDateInput(rawValue)
|
|
46210
|
+
};
|
|
46211
|
+
if (format === "time") return {
|
|
46212
|
+
type: "time",
|
|
46213
|
+
value: toTimeInput(rawValue)
|
|
46214
|
+
};
|
|
46215
|
+
return {
|
|
46216
|
+
type: "datetime-local",
|
|
46217
|
+
value: toDateTimeInput(rawValue)
|
|
46218
|
+
};
|
|
46219
|
+
}
|
|
46153
46220
|
function toDateTimeInput(value) {
|
|
46154
46221
|
return (value.includes("T") ? value : value.replace(" ", "T")).slice(0, 16);
|
|
46155
46222
|
}
|
|
@@ -46242,24 +46309,35 @@ function TextControl({ value, busy, onCommit }) {
|
|
|
46242
46309
|
className: cn("rounded border border-border bg-surface px-2 py-0.5 text-xs", busy && "opacity-40 cursor-not-allowed")
|
|
46243
46310
|
});
|
|
46244
46311
|
}
|
|
46245
|
-
function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
46312
|
+
function ControlWidget({ slice, busy, setValueFn, layout, quantity, display, dateTimeShape }) {
|
|
46246
46313
|
const t = useUiText();
|
|
46247
46314
|
switch (slice.kind) {
|
|
46248
|
-
case "numeric":
|
|
46249
|
-
|
|
46250
|
-
|
|
46251
|
-
|
|
46252
|
-
|
|
46253
|
-
|
|
46254
|
-
|
|
46255
|
-
|
|
46256
|
-
|
|
46257
|
-
unit
|
|
46258
|
-
|
|
46259
|
-
|
|
46260
|
-
|
|
46261
|
-
|
|
46262
|
-
|
|
46315
|
+
case "numeric": {
|
|
46316
|
+
const numericValue = typeof slice.value === "number" ? slice.value : Number(slice.value);
|
|
46317
|
+
const resolved = quantity === void 0 ? void 0 : resolveSensorDisplay({
|
|
46318
|
+
quantity,
|
|
46319
|
+
display,
|
|
46320
|
+
capName: "control"
|
|
46321
|
+
});
|
|
46322
|
+
const toShown = resolved?.toDisplayValue ?? ((raw) => raw);
|
|
46323
|
+
const toBase = resolved?.fromDisplayValue ?? ((shown) => shown);
|
|
46324
|
+
const unit = resolved !== void 0 && resolved.unit.length > 0 ? resolved.unit : void 0;
|
|
46325
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumericControl, {
|
|
46326
|
+
value: toShown(numericValue),
|
|
46327
|
+
busy,
|
|
46328
|
+
onCommit: (v) => {
|
|
46329
|
+
setValueFn({ control: {
|
|
46330
|
+
kind: "numeric",
|
|
46331
|
+
value: toBase(v)
|
|
46332
|
+
} });
|
|
46333
|
+
},
|
|
46334
|
+
unit,
|
|
46335
|
+
min: slice.min === void 0 ? void 0 : toShown(slice.min),
|
|
46336
|
+
max: slice.max === void 0 ? void 0 : toShown(slice.max),
|
|
46337
|
+
step: slice.step === void 0 ? void 0 : toShown(slice.step),
|
|
46338
|
+
precision: resolved?.precision
|
|
46339
|
+
});
|
|
46340
|
+
}
|
|
46263
46341
|
case "select": {
|
|
46264
46342
|
const selectValue = String(slice.value);
|
|
46265
46343
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
@@ -46292,7 +46370,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
|
46292
46370
|
});
|
|
46293
46371
|
case "datetime": {
|
|
46294
46372
|
const rawValue = String(slice.value);
|
|
46295
|
-
const format =
|
|
46373
|
+
const format = dateTimeShape ?? "datetime";
|
|
46296
46374
|
if (layout === "compact") {
|
|
46297
46375
|
if (rawValue === "") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
46298
46376
|
"aria-label": t("uiLibrary.controlPanel.value", "Control value"),
|
|
@@ -46305,8 +46383,9 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
|
46305
46383
|
children: formatControlDateTime(rawValue, format)
|
|
46306
46384
|
});
|
|
46307
46385
|
}
|
|
46308
|
-
const
|
|
46309
|
-
const
|
|
46386
|
+
const editor = datetimeEditor(format, rawValue);
|
|
46387
|
+
const inputType = editor.type;
|
|
46388
|
+
const inputValue = editor.value;
|
|
46310
46389
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
46311
46390
|
type: inputType,
|
|
46312
46391
|
"aria-label": t("uiLibrary.controlPanel.value", "Control value"),
|
|
@@ -46326,6 +46405,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
|
46326
46405
|
}
|
|
46327
46406
|
}
|
|
46328
46407
|
function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic }) {
|
|
46408
|
+
const info = useOptionalSystem()?.getDeviceInfo(deviceId) ?? null;
|
|
46329
46409
|
const { cap, slice, available } = useDeviceCapability({
|
|
46330
46410
|
trpc,
|
|
46331
46411
|
deviceId,
|
|
@@ -46335,6 +46415,8 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
|
|
|
46335
46415
|
const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
|
|
46336
46416
|
if (display === void 0) return null;
|
|
46337
46417
|
const busy = !available;
|
|
46418
|
+
const chosen = info?.sourceInfo?.quantity;
|
|
46419
|
+
const quantity = (0, _camstack_types.isReadingQuantity)(chosen) && chosen !== "device" && chosen !== "none" ? chosen : void 0;
|
|
46338
46420
|
const setValueFn = (input) => {
|
|
46339
46421
|
if (!busy && cap) {
|
|
46340
46422
|
applyOptimistic({ value: input.control.value });
|
|
@@ -46348,7 +46430,10 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
|
|
|
46348
46430
|
slice: display,
|
|
46349
46431
|
busy,
|
|
46350
46432
|
setValueFn,
|
|
46351
|
-
layout
|
|
46433
|
+
layout,
|
|
46434
|
+
quantity,
|
|
46435
|
+
display: info?.display,
|
|
46436
|
+
dateTimeShape: info?.sourceInfo?.dateTimeShape
|
|
46352
46437
|
})
|
|
46353
46438
|
});
|
|
46354
46439
|
}
|
|
@@ -46452,30 +46537,6 @@ function PopoverRowAction({ icon: Icon, title, children }) {
|
|
|
46452
46537
|
}), document.body)] });
|
|
46453
46538
|
}
|
|
46454
46539
|
//#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
46540
|
//#region src/composites/device-controls/sensor-value-atom.tsx
|
|
46480
46541
|
/**
|
|
46481
46542
|
* Binary fallback for the `battery-sensor` role.
|
|
@@ -46516,102 +46577,105 @@ function hasBatteryNumericValue(slice, field) {
|
|
|
46516
46577
|
* avoids dynamic string indexing — each one is a concrete property
|
|
46517
46578
|
* access on the typed `DeviceProxy['state']` shape.
|
|
46518
46579
|
*
|
|
46519
|
-
*
|
|
46520
|
-
*
|
|
46521
|
-
*
|
|
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.
|
|
46580
|
+
* The unit comes from the descriptor's quantity through `resolveSensorDisplay`.
|
|
46581
|
+
* A generic numeric sensor uses quantity `device` and the device's
|
|
46582
|
+
* `sourceInfo.quantity`.
|
|
46526
46583
|
*/
|
|
46527
46584
|
var ROLE_DESCRIPTOR = {
|
|
46528
46585
|
"temperature-sensor": {
|
|
46529
46586
|
kind: "numeric",
|
|
46530
46587
|
field: "celsius",
|
|
46531
|
-
|
|
46588
|
+
quantity: "temperature",
|
|
46532
46589
|
getSliceHandle: (s) => s.temperatureSensor
|
|
46533
46590
|
},
|
|
46534
46591
|
"humidity-sensor": {
|
|
46535
46592
|
kind: "numeric",
|
|
46536
46593
|
field: "percent",
|
|
46537
|
-
|
|
46594
|
+
quantity: "humidity",
|
|
46538
46595
|
getSliceHandle: (s) => s.humiditySensor
|
|
46539
46596
|
},
|
|
46540
46597
|
"ambient-light-sensor": {
|
|
46541
46598
|
kind: "numeric",
|
|
46542
46599
|
field: "lux",
|
|
46543
|
-
|
|
46600
|
+
quantity: "illuminance",
|
|
46544
46601
|
getSliceHandle: (s) => s.ambientLightSensor
|
|
46545
46602
|
},
|
|
46546
46603
|
"pressure-sensor": {
|
|
46547
46604
|
kind: "numeric",
|
|
46548
46605
|
field: "hpa",
|
|
46549
|
-
|
|
46606
|
+
quantity: "pressure",
|
|
46550
46607
|
getSliceHandle: (s) => s.pressureSensor
|
|
46551
46608
|
},
|
|
46552
46609
|
"numeric-sensor": {
|
|
46553
46610
|
kind: "numeric",
|
|
46554
46611
|
field: "value",
|
|
46612
|
+
quantity: "device",
|
|
46555
46613
|
getSliceHandle: (s) => s.numericSensor
|
|
46556
46614
|
},
|
|
46557
46615
|
"wind-sensor": {
|
|
46558
46616
|
kind: "numeric",
|
|
46559
46617
|
field: "value",
|
|
46618
|
+
quantity: "device",
|
|
46560
46619
|
getSliceHandle: (s) => s.numericSensor
|
|
46561
46620
|
},
|
|
46562
46621
|
"rain-sensor": {
|
|
46563
46622
|
kind: "numeric",
|
|
46564
46623
|
field: "value",
|
|
46624
|
+
quantity: "device",
|
|
46565
46625
|
getSliceHandle: (s) => s.numericSensor
|
|
46566
46626
|
},
|
|
46567
46627
|
"uv-sensor": {
|
|
46568
46628
|
kind: "numeric",
|
|
46569
46629
|
field: "value",
|
|
46630
|
+
quantity: "device",
|
|
46570
46631
|
getSliceHandle: (s) => s.numericSensor
|
|
46571
46632
|
},
|
|
46572
46633
|
"solar-radiation-sensor": {
|
|
46573
46634
|
kind: "numeric",
|
|
46574
46635
|
field: "value",
|
|
46636
|
+
quantity: "device",
|
|
46575
46637
|
getSliceHandle: (s) => s.numericSensor
|
|
46576
46638
|
},
|
|
46577
46639
|
"soil-moisture-sensor": {
|
|
46578
46640
|
kind: "numeric",
|
|
46579
46641
|
field: "value",
|
|
46642
|
+
quantity: "device",
|
|
46580
46643
|
getSliceHandle: (s) => s.numericSensor
|
|
46581
46644
|
},
|
|
46582
46645
|
"power-sensor": {
|
|
46583
46646
|
kind: "numeric",
|
|
46584
46647
|
field: "watts",
|
|
46585
|
-
|
|
46648
|
+
quantity: "power",
|
|
46586
46649
|
getSliceHandle: (s) => s.powerMeter
|
|
46587
46650
|
},
|
|
46588
46651
|
"energy-sensor": {
|
|
46589
46652
|
kind: "numeric",
|
|
46590
46653
|
field: "kwhTotal",
|
|
46591
|
-
|
|
46654
|
+
quantity: "energy",
|
|
46592
46655
|
getSliceHandle: (s) => s.powerMeter
|
|
46593
46656
|
},
|
|
46594
46657
|
"voltage-sensor": {
|
|
46595
46658
|
kind: "numeric",
|
|
46596
46659
|
field: "volts",
|
|
46597
|
-
|
|
46660
|
+
quantity: "voltage",
|
|
46598
46661
|
getSliceHandle: (s) => s.powerMeter
|
|
46599
46662
|
},
|
|
46600
46663
|
"current-sensor": {
|
|
46601
46664
|
kind: "numeric",
|
|
46602
46665
|
field: "amps",
|
|
46603
|
-
|
|
46666
|
+
quantity: "current",
|
|
46604
46667
|
getSliceHandle: (s) => s.powerMeter
|
|
46605
46668
|
},
|
|
46606
46669
|
"battery-sensor": {
|
|
46607
46670
|
kind: "numeric",
|
|
46608
46671
|
field: "value",
|
|
46609
|
-
|
|
46672
|
+
quantity: "percentage",
|
|
46610
46673
|
getSliceHandle: (s) => s.numericSensor
|
|
46611
46674
|
},
|
|
46612
46675
|
"generic-sensor": {
|
|
46613
46676
|
kind: "numeric",
|
|
46614
46677
|
field: "value",
|
|
46678
|
+
quantity: "device",
|
|
46615
46679
|
getSliceHandle: (s) => s.numericSensor
|
|
46616
46680
|
},
|
|
46617
46681
|
"motion-sensor": {
|
|
@@ -46729,19 +46793,16 @@ function SensorValueAtom({ trpc, device }) {
|
|
|
46729
46793
|
children: enumSensorSlice.value
|
|
46730
46794
|
});
|
|
46731
46795
|
if (numericIsColStart) return null;
|
|
46732
|
-
const
|
|
46733
|
-
const
|
|
46734
|
-
const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
|
|
46735
|
-
const resolved = resolveSensorDisplay({
|
|
46796
|
+
const deviceQuantity = device.sourceInfo?.quantity;
|
|
46797
|
+
const rendered = resolveSensorDisplay({
|
|
46736
46798
|
display: device.display,
|
|
46737
46799
|
capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
|
|
46738
46800
|
roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : void 0,
|
|
46739
|
-
|
|
46740
|
-
|
|
46741
|
-
|
|
46742
|
-
|
|
46743
|
-
const
|
|
46744
|
-
const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
|
|
46801
|
+
quantity: descriptor.quantity ?? "device",
|
|
46802
|
+
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
|
|
46803
|
+
}).format(rawValue);
|
|
46804
|
+
const unit = rendered.unit;
|
|
46805
|
+
const formatted = rendered.text;
|
|
46745
46806
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
46746
46807
|
className: cn("inline-flex items-baseline gap-0.5 tabular-nums text-xs text-foreground"),
|
|
46747
46808
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatted }), unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -46771,7 +46832,7 @@ function SensorValueAtom({ trpc, device }) {
|
|
|
46771
46832
|
if (descriptor.kind === "datetime") {
|
|
46772
46833
|
if (!isStringSliceValue(slice)) return null;
|
|
46773
46834
|
if (slice.value === "") return null;
|
|
46774
|
-
const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
46835
|
+
const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
46775
46836
|
const formatted = formatControlDateTime(slice.value, format);
|
|
46776
46837
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
46777
46838
|
className: cn("inline-flex items-baseline text-xs text-foreground"),
|
|
@@ -47326,17 +47387,14 @@ function SensorInlineControl({ trpc, deviceId }) {
|
|
|
47326
47387
|
*
|
|
47327
47388
|
* Renders:
|
|
47328
47389
|
* - Typed numeric sensors: value in `text-3xl font-semibold tabular-nums` +
|
|
47329
|
-
*
|
|
47330
|
-
* - Generic numeric-sensor:
|
|
47331
|
-
* (`slice.unit`, populated by the HA provider from state attributes).
|
|
47390
|
+
* the unit `resolveSensorDisplay` derives from the quantity.
|
|
47391
|
+
* - Generic numeric-sensor: the same formatter, with the device's quantity.
|
|
47332
47392
|
* - Binary sensors: coloured state badge ("Detected"/"Clear" etc.) — same
|
|
47333
47393
|
* labels and colours as SensorValueAtom's binary branch.
|
|
47334
47394
|
* - Presence: Home/Away/zone-name badge — same labels as SensorValueAtom.
|
|
47335
47395
|
* - All: sensor name label below the value.
|
|
47336
47396
|
*
|
|
47337
|
-
* Unit resolution uses the same
|
|
47338
|
-
* wins, ROLE_DESCRIPTOR canonicalUnit as fallback) — the two surfaces stay
|
|
47339
|
-
* in sync.
|
|
47397
|
+
* Unit resolution uses the same formatter as SensorValueAtom.
|
|
47340
47398
|
*/
|
|
47341
47399
|
function isNumericSliceValue(v) {
|
|
47342
47400
|
return typeof v === "object" && v !== null;
|
|
@@ -47403,19 +47461,16 @@ function SensorHeroInner({ trpc, device }) {
|
|
|
47403
47461
|
if (!isNumericSliceValue(slice)) return null;
|
|
47404
47462
|
const rawValue = slice[descriptor.field];
|
|
47405
47463
|
if (typeof rawValue !== "number") return null;
|
|
47406
|
-
const
|
|
47407
|
-
const
|
|
47408
|
-
const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
|
|
47409
|
-
const resolved = resolveSensorDisplay({
|
|
47464
|
+
const deviceQuantity = device.sourceInfo?.quantity;
|
|
47465
|
+
const rendered = resolveSensorDisplay({
|
|
47410
47466
|
display: device.display,
|
|
47411
47467
|
capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
|
|
47412
47468
|
roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : roleDefaultsData?.defaults["temperature-sensor"],
|
|
47413
|
-
|
|
47414
|
-
|
|
47415
|
-
|
|
47416
|
-
|
|
47417
|
-
const
|
|
47418
|
-
const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
|
|
47469
|
+
quantity: descriptor.quantity ?? "device",
|
|
47470
|
+
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
|
|
47471
|
+
}).format(rawValue);
|
|
47472
|
+
const unit = rendered.unit;
|
|
47473
|
+
const formatted = rendered.text;
|
|
47419
47474
|
const occupancyOn = occupancySlice !== void 0 && isBinarySliceValue(occupancySlice) && typeof occupancySlice["on"] === "boolean" ? Boolean(occupancySlice["on"]) : null;
|
|
47420
47475
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47421
47476
|
className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
|
|
@@ -47471,7 +47526,7 @@ function SensorHeroInner({ trpc, device }) {
|
|
|
47471
47526
|
children: t("uiLibrary.sensorHeroCard.awaiting", "Awaiting state…")
|
|
47472
47527
|
})]
|
|
47473
47528
|
});
|
|
47474
|
-
const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
47529
|
+
const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
47475
47530
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47476
47531
|
className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
|
|
47477
47532
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -49927,10 +49982,15 @@ function useWeather(trpc, deviceId) {
|
|
|
49927
49982
|
slice: (d) => d.state.weather
|
|
49928
49983
|
});
|
|
49929
49984
|
}
|
|
49930
|
-
/** Format a
|
|
49931
|
-
function
|
|
49932
|
-
if (
|
|
49933
|
-
|
|
49985
|
+
/** Format a base-unit weather reading (metric). */
|
|
49986
|
+
function formatWeather(value, quantity) {
|
|
49987
|
+
if (value === null) return "—";
|
|
49988
|
+
const rendered = (0, _camstack_types.formatReading)({
|
|
49989
|
+
value,
|
|
49990
|
+
quantity,
|
|
49991
|
+
precision: 0
|
|
49992
|
+
});
|
|
49993
|
+
return rendered.unit.length === 0 ? rendered.text : `${rendered.text}${rendered.unit}`;
|
|
49934
49994
|
}
|
|
49935
49995
|
/**
|
|
49936
49996
|
* WeatherInlineControl — compact condition icon + temperature readout
|
|
@@ -49950,7 +50010,7 @@ function WeatherInlineControl({ trpc, deviceId }) {
|
|
|
49950
50010
|
children: inlineGlyph(meta.glyph)
|
|
49951
50011
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
49952
50012
|
className: "tabular-nums",
|
|
49953
|
-
children:
|
|
50013
|
+
children: formatWeather(slice.temperature, "temperature")
|
|
49954
50014
|
})]
|
|
49955
50015
|
});
|
|
49956
50016
|
}
|
|
@@ -50260,7 +50320,11 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
50260
50320
|
if (slice === void 0) return null;
|
|
50261
50321
|
const meta = weatherConditionMeta(slice.condition, t);
|
|
50262
50322
|
const tint = weatherTint(slice.condition);
|
|
50263
|
-
const
|
|
50323
|
+
const temperature = slice.temperature === null ? void 0 : (0, _camstack_types.formatReading)({
|
|
50324
|
+
value: slice.temperature,
|
|
50325
|
+
quantity: "temperature",
|
|
50326
|
+
precision: 0
|
|
50327
|
+
});
|
|
50264
50328
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
50265
50329
|
className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4",
|
|
50266
50330
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -50275,11 +50339,14 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
50275
50339
|
children: [
|
|
50276
50340
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
50277
50341
|
className: "text-3xl font-semibold tabular-nums text-foreground",
|
|
50278
|
-
children:
|
|
50342
|
+
children: temperature !== void 0 ? temperature.text : "—"
|
|
50279
50343
|
}),
|
|
50280
50344
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
50281
50345
|
className: "text-base text-foreground-subtle",
|
|
50282
|
-
children: unit
|
|
50346
|
+
children: temperature !== void 0 ? temperature.unit : (0, _camstack_types.formatReading)({
|
|
50347
|
+
value: 0,
|
|
50348
|
+
quantity: "temperature"
|
|
50349
|
+
}).unit
|
|
50283
50350
|
}),
|
|
50284
50351
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
50285
50352
|
className: "text-xs text-foreground-subtle ml-2 capitalize",
|
|
@@ -50300,12 +50367,12 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
50300
50367
|
style: { transform: `rotate(${slice.windBearing}deg)` }
|
|
50301
50368
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
|
|
50302
50369
|
label: t("uiLibrary.weatherControl.wind", "Wind"),
|
|
50303
|
-
value:
|
|
50370
|
+
value: formatWeather(slice.windSpeed, "speed")
|
|
50304
50371
|
}),
|
|
50305
50372
|
slice.pressure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
|
|
50306
50373
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5" }),
|
|
50307
50374
|
label: t("uiLibrary.weatherControl.pressure", "Pressure"),
|
|
50308
|
-
value:
|
|
50375
|
+
value: formatWeather(slice.pressure, "pressure")
|
|
50309
50376
|
})
|
|
50310
50377
|
]
|
|
50311
50378
|
})]
|
|
@@ -50544,6 +50611,16 @@ var VOLUME_MAX = 9;
|
|
|
50544
50611
|
/** Amber kibble accent for the bowl fill; destructive red when low. */
|
|
50545
50612
|
var KIBBLE = "#f59e0b";
|
|
50546
50613
|
var KIBBLE_LOW = "#ef4444";
|
|
50614
|
+
function desiccantLabel(t, seconds) {
|
|
50615
|
+
const rendered = (0, _camstack_types.formatReading)({
|
|
50616
|
+
value: seconds,
|
|
50617
|
+
quantity: "duration"
|
|
50618
|
+
});
|
|
50619
|
+
return t("uiLibrary.petFeeder.desiccant", "{{text}}{{unit}} desiccant", {
|
|
50620
|
+
text: rendered.text,
|
|
50621
|
+
unit: rendered.unit
|
|
50622
|
+
});
|
|
50623
|
+
}
|
|
50547
50624
|
function clampPortion(grams) {
|
|
50548
50625
|
return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
|
|
50549
50626
|
}
|
|
@@ -50666,7 +50743,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
50666
50743
|
}),
|
|
50667
50744
|
display.desiccantLeftDays !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout, {
|
|
50668
50745
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
|
|
50669
|
-
label: t
|
|
50746
|
+
label: desiccantLabel(t, display.desiccantLeftDays)
|
|
50670
50747
|
}),
|
|
50671
50748
|
onBatteries && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
50672
50749
|
className: "inline-flex items-center gap-1 font-medium text-amber-500",
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as __mfHostReact from "react";
|
|
|
5
5
|
import React, { Fragment, Suspense, cloneElement, createContext, createElement, forwardRef, lazy, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
6
6
|
import * as __mfHostReactJsxRuntime from "react/jsx-runtime";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, CONTAINER_CHILD_PRIORITY, ConsumableLevelSchema, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEVICE_HISTORY_ENERGY_KEY, DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, HISTORY_CATALOG, ICON_CATEGORIES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, NODE_COLOR_IDS, NodeColorIdSchema, PALETTE_ENTRY_IDS, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, REDACTED_SECRET, RecordingConfigSchema, RecordingStatusSchema, SECRET_CLEAR, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, SUBJECT_PATH_DWELL_EPS, SUBJECT_PATH_FULL_FRAME_COVERAGE, SUBJECT_PATH_MAX_POINTS, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, assignNodeColorId, assignNodeColorId as assignNodeColorId$1, buildModelVariantGroups, buildSubjectPath, bundledCamstack, canConvertUnit, canonicalIconKey, clearModelBoundOverrides, clearModelBoundSetting, clusterStepSettingFieldsFor, collectModelBoundKeys, consumableFormOf, consumableKindOf, createDeviceProxy, createLazyTrpcSource, createSliceHandle, dehydrateSchema, deriveRecordingMode, describeModelVariant, effectiveStepSettings, findTimezone, formatParkedFor, hasModelBoundOverrides, iconCategoryLabel, iconsInCategory, isDrawableSubjectPath, isKeychainStoredField, isModelScopedStepSettingKey, isRootSuppliableStepId, isSecretClear, isSecretFieldView, isSecretMarker, modelBindingKey, normalizeUnit, paintHex, parseOccupancyScope, projectSubjectPath, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveIconKey, resolveModelBoundSetting, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchIcons, selectAssignedProfileSlots, stepSettingSource, subjectPathSampleFromBox, subjectPathSvgPoints, tryConvertUnit, unitsForDimension, withFieldFrom, withoutField, writeModelBoundSetting } from "@camstack/types";
|
|
8
|
+
import { ALL_CAPABILITY_DEFINITIONS, CAM_PROFILE_ORDER, CLIP_STILL_DISPOSITION_HEADER, CLIP_STILL_REASON_CODE_HEADER, CLIP_STILL_REASON_HEADER, CONTAINER_CHILD_PRIORITY, ConsumableLevelSchema, DEFAULT_DISPLAY_PREFERENCE, DEFAULT_EVENTS_BAND_BUFFER_SEC, DEVICE_HISTORY_ENERGY_KEY, DEVICE_HISTORY_QUERY_MANY_MAX_ITEMS, DEVICE_TYPE_CONTROL_KIND, DeviceExportStatusSchema, DeviceFeature, DeviceRole, DeviceType, EXTENSION_SLOTS, EXTENSION_SLOT_IDS, EventCategory, ExposedDeviceSchema, FIRST_LEVEL_MACRO_CLASSES, HISTORY_CATALOG, ICON_CATEGORIES, KNOWN_CAP_NAMES, MAX_SENSOR_TRIGGER_DEVICES, MODEL_PROVIDER_IDS, NAVIGATION_LIGHT_LEVEL_MAX, NAVIGATION_LIGHT_LEVEL_MIN, NODE_COLOR_IDS, NodeColorIdSchema, PALETTE_ENTRY_IDS, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, RECORDING_AUDIO_TRIGGER_OFFERED_DBFS, RECORDING_SOURCE_CAMSTACK_ADDON, REDACTED_SECRET, RecordingConfigSchema, RecordingStatusSchema, SECRET_CLEAR, SOURCE_CAP_ACTIVE_FIELD, STATIONARY_OVERLAY_STYLE, SUBJECT_PATH_DWELL_EPS, SUBJECT_PATH_FULL_FRAME_COVERAGE, SUBJECT_PATH_MAX_POINTS, TIMEZONES, USE_ROOT_MODEL_SETTING_KEY, assignNodeColorId, assignNodeColorId as assignNodeColorId$1, baseUnitOf, buildModelVariantGroups, buildSubjectPath, bundledCamstack, canConvertUnit, canonicalIconKey, clearModelBoundOverrides, clearModelBoundSetting, clusterStepSettingFieldsFor, collectModelBoundKeys, consumableFormOf, consumableKindOf, createDeviceProxy, createLazyTrpcSource, createSliceHandle, dehydrateSchema, deriveRecordingMode, describeModelVariant, displayUnitFor, effectiveStepSettings, findTimezone, formatParkedFor, formatReading, hasModelBoundOverrides, iconCategoryLabel, iconsInCategory, isDrawableSubjectPath, isKeychainStoredField, isModelScopedStepSettingKey, isReadingQuantity, isRootSuppliableStepId, isSecretClear, isSecretFieldView, isSecretMarker, modelBindingKey, normalizeUnit, paintHex, parseOccupancyScope, projectSubjectPath, publicEventCategories, resolveContainerPrimaryChild, resolveDeviceControlKind, resolveIconKey, resolveModelBoundSetting, resolveRecordingProfiles, resolveRootSupply, resolveVariantModelId, resolveViewableDeviceIds, searchIcons, selectAssignedProfileSlots, stepSettingSource, subjectPathSampleFromBox, subjectPathSvgPoints, tryConvertUnit, unitsForDimension, withFieldFrom, withoutField, writeModelBoundSetting } from "@camstack/types";
|
|
9
9
|
import * as __mfHostTrpcReactQuery from "@trpc/react-query";
|
|
10
10
|
import { createTRPCReact } from "@trpc/react-query";
|
|
11
11
|
import * as __mfHostReactDOM from "react-dom";
|
|
@@ -20949,21 +20949,16 @@ Skeleton.displayName = "Skeleton";
|
|
|
20949
20949
|
/** `value` null = unknown (D393) — renders an em-dash, never a fake `0`. */
|
|
20950
20950
|
function formatRemaining(t, r) {
|
|
20951
20951
|
if (r.value === null) return "—";
|
|
20952
|
-
|
|
20953
|
-
|
|
20954
|
-
|
|
20955
|
-
|
|
20956
|
-
|
|
20957
|
-
|
|
20958
|
-
|
|
20959
|
-
|
|
20960
|
-
|
|
20961
|
-
|
|
20962
|
-
case "cycles": return t("uiLibrary.consumablesPanel.cycles", {
|
|
20963
|
-
one: "{{count}} cycle",
|
|
20964
|
-
other: "{{count}} cycles"
|
|
20965
|
-
}, { count });
|
|
20966
|
-
}
|
|
20952
|
+
if (r.kind === "count") return t("uiLibrary.consumablesPanel.cycles", {
|
|
20953
|
+
one: "{{count}} cycle",
|
|
20954
|
+
other: "{{count}} cycles"
|
|
20955
|
+
}, { count: r.value });
|
|
20956
|
+
const rendered = formatReading({
|
|
20957
|
+
value: r.value,
|
|
20958
|
+
quantity: "duration",
|
|
20959
|
+
precision: 0
|
|
20960
|
+
});
|
|
20961
|
+
return rendered.unit.length === 0 ? rendered.text : `${rendered.text} ${rendered.unit}`;
|
|
20967
20962
|
}
|
|
20968
20963
|
/** Format a ms-epoch as a compact local date for the "replaced" hint. */
|
|
20969
20964
|
function formatResetAt(ms) {
|
|
@@ -46059,6 +46054,66 @@ function formatNumeric(value, precision) {
|
|
|
46059
46054
|
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
46060
46055
|
}
|
|
46061
46056
|
//#endregion
|
|
46057
|
+
//#region src/lib/resolve-sensor-display.ts
|
|
46058
|
+
function canonUnit(raw) {
|
|
46059
|
+
if (raw === void 0) return void 0;
|
|
46060
|
+
return normalizeUnit(raw) ?? raw;
|
|
46061
|
+
}
|
|
46062
|
+
function concreteQuantity(input) {
|
|
46063
|
+
if (input.quantity === void 0) return void 0;
|
|
46064
|
+
if (input.quantity === "device") {
|
|
46065
|
+
const chosen = input.deviceQuantity;
|
|
46066
|
+
if (chosen === void 0 || chosen === "device") return void 0;
|
|
46067
|
+
return chosen;
|
|
46068
|
+
}
|
|
46069
|
+
return input.quantity;
|
|
46070
|
+
}
|
|
46071
|
+
function resolveSensorDisplay(input) {
|
|
46072
|
+
const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
|
|
46073
|
+
const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.declarationPrecision;
|
|
46074
|
+
const quantity = concreteQuantity(input);
|
|
46075
|
+
const preference = input.preference ?? DEFAULT_DISPLAY_PREFERENCE;
|
|
46076
|
+
const base = quantity === void 0 ? void 0 : baseUnitOf(quantity);
|
|
46077
|
+
const preferred = quantity === void 0 ? void 0 : displayUnitFor(quantity, preference);
|
|
46078
|
+
const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
|
|
46079
|
+
const overrideOk = overrideUnit !== void 0 && base !== void 0 && overrideUnit !== base && canConvertUnit(base, overrideUnit);
|
|
46080
|
+
const unit = overrideOk ? overrideUnit : preferred ?? "";
|
|
46081
|
+
const scales = quantity === "duration" || quantity === "information" || quantity === "duration-ms";
|
|
46082
|
+
const toDisplayValue = (raw) => {
|
|
46083
|
+
if (scales || base === void 0 || unit.length === 0 || unit === base) return raw;
|
|
46084
|
+
return tryConvertUnit(raw, base, unit) ?? raw;
|
|
46085
|
+
};
|
|
46086
|
+
const fromDisplayValue = (shown) => {
|
|
46087
|
+
if (scales || base === void 0 || unit.length === 0 || unit === base) return shown;
|
|
46088
|
+
return tryConvertUnit(shown, unit, base) ?? shown;
|
|
46089
|
+
};
|
|
46090
|
+
const format = (raw) => {
|
|
46091
|
+
if (quantity !== void 0 && !overrideOk) {
|
|
46092
|
+
const rendered = formatReading({
|
|
46093
|
+
value: raw,
|
|
46094
|
+
quantity,
|
|
46095
|
+
preference,
|
|
46096
|
+
precision
|
|
46097
|
+
});
|
|
46098
|
+
return {
|
|
46099
|
+
text: rendered.text,
|
|
46100
|
+
unit: rendered.unit
|
|
46101
|
+
};
|
|
46102
|
+
}
|
|
46103
|
+
return {
|
|
46104
|
+
text: formatNumeric(toDisplayValue(raw), precision),
|
|
46105
|
+
unit
|
|
46106
|
+
};
|
|
46107
|
+
};
|
|
46108
|
+
return {
|
|
46109
|
+
unit,
|
|
46110
|
+
precision,
|
|
46111
|
+
toDisplayValue,
|
|
46112
|
+
fromDisplayValue,
|
|
46113
|
+
format
|
|
46114
|
+
};
|
|
46115
|
+
}
|
|
46116
|
+
//#endregion
|
|
46062
46117
|
//#region src/lib/format-control-datetime.ts
|
|
46063
46118
|
var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
46064
46119
|
var TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
@@ -46124,8 +46179,20 @@ function toDateInput(value) {
|
|
|
46124
46179
|
function toTimeInput(value) {
|
|
46125
46180
|
return value.slice(0, 5);
|
|
46126
46181
|
}
|
|
46127
|
-
|
|
46128
|
-
|
|
46182
|
+
function datetimeEditor(format, rawValue) {
|
|
46183
|
+
if (format === "date") return {
|
|
46184
|
+
type: "date",
|
|
46185
|
+
value: toDateInput(rawValue)
|
|
46186
|
+
};
|
|
46187
|
+
if (format === "time") return {
|
|
46188
|
+
type: "time",
|
|
46189
|
+
value: toTimeInput(rawValue)
|
|
46190
|
+
};
|
|
46191
|
+
return {
|
|
46192
|
+
type: "datetime-local",
|
|
46193
|
+
value: toDateTimeInput(rawValue)
|
|
46194
|
+
};
|
|
46195
|
+
}
|
|
46129
46196
|
function toDateTimeInput(value) {
|
|
46130
46197
|
return (value.includes("T") ? value : value.replace(" ", "T")).slice(0, 16);
|
|
46131
46198
|
}
|
|
@@ -46218,24 +46285,35 @@ function TextControl({ value, busy, onCommit }) {
|
|
|
46218
46285
|
className: cn("rounded border border-border bg-surface px-2 py-0.5 text-xs", busy && "opacity-40 cursor-not-allowed")
|
|
46219
46286
|
});
|
|
46220
46287
|
}
|
|
46221
|
-
function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
46288
|
+
function ControlWidget({ slice, busy, setValueFn, layout, quantity, display, dateTimeShape }) {
|
|
46222
46289
|
const t = useUiText();
|
|
46223
46290
|
switch (slice.kind) {
|
|
46224
|
-
case "numeric":
|
|
46225
|
-
|
|
46226
|
-
|
|
46227
|
-
|
|
46228
|
-
|
|
46229
|
-
|
|
46230
|
-
|
|
46231
|
-
|
|
46232
|
-
|
|
46233
|
-
unit
|
|
46234
|
-
|
|
46235
|
-
|
|
46236
|
-
|
|
46237
|
-
|
|
46238
|
-
|
|
46291
|
+
case "numeric": {
|
|
46292
|
+
const numericValue = typeof slice.value === "number" ? slice.value : Number(slice.value);
|
|
46293
|
+
const resolved = quantity === void 0 ? void 0 : resolveSensorDisplay({
|
|
46294
|
+
quantity,
|
|
46295
|
+
display,
|
|
46296
|
+
capName: "control"
|
|
46297
|
+
});
|
|
46298
|
+
const toShown = resolved?.toDisplayValue ?? ((raw) => raw);
|
|
46299
|
+
const toBase = resolved?.fromDisplayValue ?? ((shown) => shown);
|
|
46300
|
+
const unit = resolved !== void 0 && resolved.unit.length > 0 ? resolved.unit : void 0;
|
|
46301
|
+
return /* @__PURE__ */ jsx(NumericControl, {
|
|
46302
|
+
value: toShown(numericValue),
|
|
46303
|
+
busy,
|
|
46304
|
+
onCommit: (v) => {
|
|
46305
|
+
setValueFn({ control: {
|
|
46306
|
+
kind: "numeric",
|
|
46307
|
+
value: toBase(v)
|
|
46308
|
+
} });
|
|
46309
|
+
},
|
|
46310
|
+
unit,
|
|
46311
|
+
min: slice.min === void 0 ? void 0 : toShown(slice.min),
|
|
46312
|
+
max: slice.max === void 0 ? void 0 : toShown(slice.max),
|
|
46313
|
+
step: slice.step === void 0 ? void 0 : toShown(slice.step),
|
|
46314
|
+
precision: resolved?.precision
|
|
46315
|
+
});
|
|
46316
|
+
}
|
|
46239
46317
|
case "select": {
|
|
46240
46318
|
const selectValue = String(slice.value);
|
|
46241
46319
|
return /* @__PURE__ */ jsx("select", {
|
|
@@ -46268,7 +46346,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
|
46268
46346
|
});
|
|
46269
46347
|
case "datetime": {
|
|
46270
46348
|
const rawValue = String(slice.value);
|
|
46271
|
-
const format =
|
|
46349
|
+
const format = dateTimeShape ?? "datetime";
|
|
46272
46350
|
if (layout === "compact") {
|
|
46273
46351
|
if (rawValue === "") return /* @__PURE__ */ jsx("span", {
|
|
46274
46352
|
"aria-label": t("uiLibrary.controlPanel.value", "Control value"),
|
|
@@ -46281,8 +46359,9 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
|
46281
46359
|
children: formatControlDateTime(rawValue, format)
|
|
46282
46360
|
});
|
|
46283
46361
|
}
|
|
46284
|
-
const
|
|
46285
|
-
const
|
|
46362
|
+
const editor = datetimeEditor(format, rawValue);
|
|
46363
|
+
const inputType = editor.type;
|
|
46364
|
+
const inputValue = editor.value;
|
|
46286
46365
|
return /* @__PURE__ */ jsx("input", {
|
|
46287
46366
|
type: inputType,
|
|
46288
46367
|
"aria-label": t("uiLibrary.controlPanel.value", "Control value"),
|
|
@@ -46302,6 +46381,7 @@ function ControlWidget({ slice, busy, setValueFn, layout }) {
|
|
|
46302
46381
|
}
|
|
46303
46382
|
}
|
|
46304
46383
|
function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic }) {
|
|
46384
|
+
const info = useOptionalSystem()?.getDeviceInfo(deviceId) ?? null;
|
|
46305
46385
|
const { cap, slice, available } = useDeviceCapability({
|
|
46306
46386
|
trpc,
|
|
46307
46387
|
deviceId,
|
|
@@ -46311,6 +46391,8 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
|
|
|
46311
46391
|
const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
|
|
46312
46392
|
if (display === void 0) return null;
|
|
46313
46393
|
const busy = !available;
|
|
46394
|
+
const chosen = info?.sourceInfo?.quantity;
|
|
46395
|
+
const quantity = isReadingQuantity(chosen) && chosen !== "device" && chosen !== "none" ? chosen : void 0;
|
|
46314
46396
|
const setValueFn = (input) => {
|
|
46315
46397
|
if (!busy && cap) {
|
|
46316
46398
|
applyOptimistic({ value: input.control.value });
|
|
@@ -46324,7 +46406,10 @@ function ControlPanel({ trpc, deviceId, features: _features, layout, optimistic
|
|
|
46324
46406
|
slice: display,
|
|
46325
46407
|
busy,
|
|
46326
46408
|
setValueFn,
|
|
46327
|
-
layout
|
|
46409
|
+
layout,
|
|
46410
|
+
quantity,
|
|
46411
|
+
display: info?.display,
|
|
46412
|
+
dateTimeShape: info?.sourceInfo?.dateTimeShape
|
|
46328
46413
|
})
|
|
46329
46414
|
});
|
|
46330
46415
|
}
|
|
@@ -46428,30 +46513,6 @@ function PopoverRowAction({ icon: Icon, title, children }) {
|
|
|
46428
46513
|
}), document.body)] });
|
|
46429
46514
|
}
|
|
46430
46515
|
//#endregion
|
|
46431
|
-
//#region src/lib/resolve-sensor-display.ts
|
|
46432
|
-
/** Table keys are canonical spellings; providers often send `C` / `F` / `celsius`. */
|
|
46433
|
-
function canonUnit(raw) {
|
|
46434
|
-
if (raw === void 0) return void 0;
|
|
46435
|
-
return normalizeUnit(raw) ?? raw;
|
|
46436
|
-
}
|
|
46437
|
-
var identity = (raw) => raw;
|
|
46438
|
-
function resolveSensorDisplay(input) {
|
|
46439
|
-
const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
|
|
46440
|
-
const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.slicePrecision;
|
|
46441
|
-
const sourceUnit = canonUnit(input.sliceUnit ?? input.canonicalUnit);
|
|
46442
|
-
const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
|
|
46443
|
-
if (overrideUnit !== void 0 && sourceUnit !== void 0 && overrideUnit !== sourceUnit && canConvertUnit(sourceUnit, overrideUnit)) return {
|
|
46444
|
-
unit: overrideUnit,
|
|
46445
|
-
precision,
|
|
46446
|
-
toDisplayValue: (raw) => tryConvertUnit(raw, sourceUnit, overrideUnit) ?? raw
|
|
46447
|
-
};
|
|
46448
|
-
return {
|
|
46449
|
-
unit: sourceUnit ?? "",
|
|
46450
|
-
precision,
|
|
46451
|
-
toDisplayValue: identity
|
|
46452
|
-
};
|
|
46453
|
-
}
|
|
46454
|
-
//#endregion
|
|
46455
46516
|
//#region src/composites/device-controls/sensor-value-atom.tsx
|
|
46456
46517
|
/**
|
|
46457
46518
|
* Binary fallback for the `battery-sensor` role.
|
|
@@ -46492,102 +46553,105 @@ function hasBatteryNumericValue(slice, field) {
|
|
|
46492
46553
|
* avoids dynamic string indexing — each one is a concrete property
|
|
46493
46554
|
* access on the typed `DeviceProxy['state']` shape.
|
|
46494
46555
|
*
|
|
46495
|
-
*
|
|
46496
|
-
*
|
|
46497
|
-
*
|
|
46498
|
-
* so no per-device lookup is needed.
|
|
46499
|
-
* - Generic `numeric-sensor` has no `canonicalUnit`. The unit is read live
|
|
46500
|
-
* from the slice (`slice.unit`) as populated by the provider from the
|
|
46501
|
-
* upstream source on every state push.
|
|
46556
|
+
* The unit comes from the descriptor's quantity through `resolveSensorDisplay`.
|
|
46557
|
+
* A generic numeric sensor uses quantity `device` and the device's
|
|
46558
|
+
* `sourceInfo.quantity`.
|
|
46502
46559
|
*/
|
|
46503
46560
|
var ROLE_DESCRIPTOR = {
|
|
46504
46561
|
"temperature-sensor": {
|
|
46505
46562
|
kind: "numeric",
|
|
46506
46563
|
field: "celsius",
|
|
46507
|
-
|
|
46564
|
+
quantity: "temperature",
|
|
46508
46565
|
getSliceHandle: (s) => s.temperatureSensor
|
|
46509
46566
|
},
|
|
46510
46567
|
"humidity-sensor": {
|
|
46511
46568
|
kind: "numeric",
|
|
46512
46569
|
field: "percent",
|
|
46513
|
-
|
|
46570
|
+
quantity: "humidity",
|
|
46514
46571
|
getSliceHandle: (s) => s.humiditySensor
|
|
46515
46572
|
},
|
|
46516
46573
|
"ambient-light-sensor": {
|
|
46517
46574
|
kind: "numeric",
|
|
46518
46575
|
field: "lux",
|
|
46519
|
-
|
|
46576
|
+
quantity: "illuminance",
|
|
46520
46577
|
getSliceHandle: (s) => s.ambientLightSensor
|
|
46521
46578
|
},
|
|
46522
46579
|
"pressure-sensor": {
|
|
46523
46580
|
kind: "numeric",
|
|
46524
46581
|
field: "hpa",
|
|
46525
|
-
|
|
46582
|
+
quantity: "pressure",
|
|
46526
46583
|
getSliceHandle: (s) => s.pressureSensor
|
|
46527
46584
|
},
|
|
46528
46585
|
"numeric-sensor": {
|
|
46529
46586
|
kind: "numeric",
|
|
46530
46587
|
field: "value",
|
|
46588
|
+
quantity: "device",
|
|
46531
46589
|
getSliceHandle: (s) => s.numericSensor
|
|
46532
46590
|
},
|
|
46533
46591
|
"wind-sensor": {
|
|
46534
46592
|
kind: "numeric",
|
|
46535
46593
|
field: "value",
|
|
46594
|
+
quantity: "device",
|
|
46536
46595
|
getSliceHandle: (s) => s.numericSensor
|
|
46537
46596
|
},
|
|
46538
46597
|
"rain-sensor": {
|
|
46539
46598
|
kind: "numeric",
|
|
46540
46599
|
field: "value",
|
|
46600
|
+
quantity: "device",
|
|
46541
46601
|
getSliceHandle: (s) => s.numericSensor
|
|
46542
46602
|
},
|
|
46543
46603
|
"uv-sensor": {
|
|
46544
46604
|
kind: "numeric",
|
|
46545
46605
|
field: "value",
|
|
46606
|
+
quantity: "device",
|
|
46546
46607
|
getSliceHandle: (s) => s.numericSensor
|
|
46547
46608
|
},
|
|
46548
46609
|
"solar-radiation-sensor": {
|
|
46549
46610
|
kind: "numeric",
|
|
46550
46611
|
field: "value",
|
|
46612
|
+
quantity: "device",
|
|
46551
46613
|
getSliceHandle: (s) => s.numericSensor
|
|
46552
46614
|
},
|
|
46553
46615
|
"soil-moisture-sensor": {
|
|
46554
46616
|
kind: "numeric",
|
|
46555
46617
|
field: "value",
|
|
46618
|
+
quantity: "device",
|
|
46556
46619
|
getSliceHandle: (s) => s.numericSensor
|
|
46557
46620
|
},
|
|
46558
46621
|
"power-sensor": {
|
|
46559
46622
|
kind: "numeric",
|
|
46560
46623
|
field: "watts",
|
|
46561
|
-
|
|
46624
|
+
quantity: "power",
|
|
46562
46625
|
getSliceHandle: (s) => s.powerMeter
|
|
46563
46626
|
},
|
|
46564
46627
|
"energy-sensor": {
|
|
46565
46628
|
kind: "numeric",
|
|
46566
46629
|
field: "kwhTotal",
|
|
46567
|
-
|
|
46630
|
+
quantity: "energy",
|
|
46568
46631
|
getSliceHandle: (s) => s.powerMeter
|
|
46569
46632
|
},
|
|
46570
46633
|
"voltage-sensor": {
|
|
46571
46634
|
kind: "numeric",
|
|
46572
46635
|
field: "volts",
|
|
46573
|
-
|
|
46636
|
+
quantity: "voltage",
|
|
46574
46637
|
getSliceHandle: (s) => s.powerMeter
|
|
46575
46638
|
},
|
|
46576
46639
|
"current-sensor": {
|
|
46577
46640
|
kind: "numeric",
|
|
46578
46641
|
field: "amps",
|
|
46579
|
-
|
|
46642
|
+
quantity: "current",
|
|
46580
46643
|
getSliceHandle: (s) => s.powerMeter
|
|
46581
46644
|
},
|
|
46582
46645
|
"battery-sensor": {
|
|
46583
46646
|
kind: "numeric",
|
|
46584
46647
|
field: "value",
|
|
46585
|
-
|
|
46648
|
+
quantity: "percentage",
|
|
46586
46649
|
getSliceHandle: (s) => s.numericSensor
|
|
46587
46650
|
},
|
|
46588
46651
|
"generic-sensor": {
|
|
46589
46652
|
kind: "numeric",
|
|
46590
46653
|
field: "value",
|
|
46654
|
+
quantity: "device",
|
|
46591
46655
|
getSliceHandle: (s) => s.numericSensor
|
|
46592
46656
|
},
|
|
46593
46657
|
"motion-sensor": {
|
|
@@ -46705,19 +46769,16 @@ function SensorValueAtom({ trpc, device }) {
|
|
|
46705
46769
|
children: enumSensorSlice.value
|
|
46706
46770
|
});
|
|
46707
46771
|
if (numericIsColStart) return null;
|
|
46708
|
-
const
|
|
46709
|
-
const
|
|
46710
|
-
const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
|
|
46711
|
-
const resolved = resolveSensorDisplay({
|
|
46772
|
+
const deviceQuantity = device.sourceInfo?.quantity;
|
|
46773
|
+
const rendered = resolveSensorDisplay({
|
|
46712
46774
|
display: device.display,
|
|
46713
46775
|
capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
|
|
46714
46776
|
roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : void 0,
|
|
46715
|
-
|
|
46716
|
-
|
|
46717
|
-
|
|
46718
|
-
|
|
46719
|
-
const
|
|
46720
|
-
const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
|
|
46777
|
+
quantity: descriptor.quantity ?? "device",
|
|
46778
|
+
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
|
|
46779
|
+
}).format(rawValue);
|
|
46780
|
+
const unit = rendered.unit;
|
|
46781
|
+
const formatted = rendered.text;
|
|
46721
46782
|
return /* @__PURE__ */ jsxs("span", {
|
|
46722
46783
|
className: cn("inline-flex items-baseline gap-0.5 tabular-nums text-xs text-foreground"),
|
|
46723
46784
|
children: [/* @__PURE__ */ jsx("span", { children: formatted }), unit && /* @__PURE__ */ jsx("span", {
|
|
@@ -46747,7 +46808,7 @@ function SensorValueAtom({ trpc, device }) {
|
|
|
46747
46808
|
if (descriptor.kind === "datetime") {
|
|
46748
46809
|
if (!isStringSliceValue(slice)) return null;
|
|
46749
46810
|
if (slice.value === "") return null;
|
|
46750
|
-
const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
46811
|
+
const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
46751
46812
|
const formatted = formatControlDateTime(slice.value, format);
|
|
46752
46813
|
return /* @__PURE__ */ jsx("span", {
|
|
46753
46814
|
className: cn("inline-flex items-baseline text-xs text-foreground"),
|
|
@@ -47302,17 +47363,14 @@ function SensorInlineControl({ trpc, deviceId }) {
|
|
|
47302
47363
|
*
|
|
47303
47364
|
* Renders:
|
|
47304
47365
|
* - Typed numeric sensors: value in `text-3xl font-semibold tabular-nums` +
|
|
47305
|
-
*
|
|
47306
|
-
* - Generic numeric-sensor:
|
|
47307
|
-
* (`slice.unit`, populated by the HA provider from state attributes).
|
|
47366
|
+
* the unit `resolveSensorDisplay` derives from the quantity.
|
|
47367
|
+
* - Generic numeric-sensor: the same formatter, with the device's quantity.
|
|
47308
47368
|
* - Binary sensors: coloured state badge ("Detected"/"Clear" etc.) — same
|
|
47309
47369
|
* labels and colours as SensorValueAtom's binary branch.
|
|
47310
47370
|
* - Presence: Home/Away/zone-name badge — same labels as SensorValueAtom.
|
|
47311
47371
|
* - All: sensor name label below the value.
|
|
47312
47372
|
*
|
|
47313
|
-
* Unit resolution uses the same
|
|
47314
|
-
* wins, ROLE_DESCRIPTOR canonicalUnit as fallback) — the two surfaces stay
|
|
47315
|
-
* in sync.
|
|
47373
|
+
* Unit resolution uses the same formatter as SensorValueAtom.
|
|
47316
47374
|
*/
|
|
47317
47375
|
function isNumericSliceValue(v) {
|
|
47318
47376
|
return typeof v === "object" && v !== null;
|
|
@@ -47379,19 +47437,16 @@ function SensorHeroInner({ trpc, device }) {
|
|
|
47379
47437
|
if (!isNumericSliceValue(slice)) return null;
|
|
47380
47438
|
const rawValue = slice[descriptor.field];
|
|
47381
47439
|
if (typeof rawValue !== "number") return null;
|
|
47382
|
-
const
|
|
47383
|
-
const
|
|
47384
|
-
const slicePrecision = typeof sliceRecord["precision"] === "number" ? sliceRecord["precision"] : void 0;
|
|
47385
|
-
const resolved = resolveSensorDisplay({
|
|
47440
|
+
const deviceQuantity = device.sourceInfo?.quantity;
|
|
47441
|
+
const rendered = resolveSensorDisplay({
|
|
47386
47442
|
display: device.display,
|
|
47387
47443
|
capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
|
|
47388
47444
|
roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : roleDefaultsData?.defaults["temperature-sensor"],
|
|
47389
|
-
|
|
47390
|
-
|
|
47391
|
-
|
|
47392
|
-
|
|
47393
|
-
const
|
|
47394
|
-
const formatted = formatNumeric(resolved.toDisplayValue(rawValue), resolved.precision);
|
|
47445
|
+
quantity: descriptor.quantity ?? "device",
|
|
47446
|
+
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
|
|
47447
|
+
}).format(rawValue);
|
|
47448
|
+
const unit = rendered.unit;
|
|
47449
|
+
const formatted = rendered.text;
|
|
47395
47450
|
const occupancyOn = occupancySlice !== void 0 && isBinarySliceValue(occupancySlice) && typeof occupancySlice["on"] === "boolean" ? Boolean(occupancySlice["on"]) : null;
|
|
47396
47451
|
return /* @__PURE__ */ jsxs("div", {
|
|
47397
47452
|
className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
|
|
@@ -47447,7 +47502,7 @@ function SensorHeroInner({ trpc, device }) {
|
|
|
47447
47502
|
children: t("uiLibrary.sensorHeroCard.awaiting", "Awaiting state…")
|
|
47448
47503
|
})]
|
|
47449
47504
|
});
|
|
47450
|
-
const format = readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
47505
|
+
const format = device.sourceInfo?.dateTimeShape ?? readSliceDateTimeFormat(slice) ?? descriptor.format;
|
|
47451
47506
|
return /* @__PURE__ */ jsxs("div", {
|
|
47452
47507
|
className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-border bg-surface p-4",
|
|
47453
47508
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -49903,10 +49958,15 @@ function useWeather(trpc, deviceId) {
|
|
|
49903
49958
|
slice: (d) => d.state.weather
|
|
49904
49959
|
});
|
|
49905
49960
|
}
|
|
49906
|
-
/** Format a
|
|
49907
|
-
function
|
|
49908
|
-
if (
|
|
49909
|
-
|
|
49961
|
+
/** Format a base-unit weather reading (metric). */
|
|
49962
|
+
function formatWeather(value, quantity) {
|
|
49963
|
+
if (value === null) return "—";
|
|
49964
|
+
const rendered = formatReading({
|
|
49965
|
+
value,
|
|
49966
|
+
quantity,
|
|
49967
|
+
precision: 0
|
|
49968
|
+
});
|
|
49969
|
+
return rendered.unit.length === 0 ? rendered.text : `${rendered.text}${rendered.unit}`;
|
|
49910
49970
|
}
|
|
49911
49971
|
/**
|
|
49912
49972
|
* WeatherInlineControl — compact condition icon + temperature readout
|
|
@@ -49926,7 +49986,7 @@ function WeatherInlineControl({ trpc, deviceId }) {
|
|
|
49926
49986
|
children: inlineGlyph(meta.glyph)
|
|
49927
49987
|
}), /* @__PURE__ */ jsx("span", {
|
|
49928
49988
|
className: "tabular-nums",
|
|
49929
|
-
children:
|
|
49989
|
+
children: formatWeather(slice.temperature, "temperature")
|
|
49930
49990
|
})]
|
|
49931
49991
|
});
|
|
49932
49992
|
}
|
|
@@ -50236,7 +50296,11 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
50236
50296
|
if (slice === void 0) return null;
|
|
50237
50297
|
const meta = weatherConditionMeta(slice.condition, t);
|
|
50238
50298
|
const tint = weatherTint(slice.condition);
|
|
50239
|
-
const
|
|
50299
|
+
const temperature = slice.temperature === null ? void 0 : formatReading({
|
|
50300
|
+
value: slice.temperature,
|
|
50301
|
+
quantity: "temperature",
|
|
50302
|
+
precision: 0
|
|
50303
|
+
});
|
|
50240
50304
|
return /* @__PURE__ */ jsx("div", {
|
|
50241
50305
|
className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4",
|
|
50242
50306
|
children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -50251,11 +50315,14 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
50251
50315
|
children: [
|
|
50252
50316
|
/* @__PURE__ */ jsx("span", {
|
|
50253
50317
|
className: "text-3xl font-semibold tabular-nums text-foreground",
|
|
50254
|
-
children:
|
|
50318
|
+
children: temperature !== void 0 ? temperature.text : "—"
|
|
50255
50319
|
}),
|
|
50256
50320
|
/* @__PURE__ */ jsx("span", {
|
|
50257
50321
|
className: "text-base text-foreground-subtle",
|
|
50258
|
-
children: unit
|
|
50322
|
+
children: temperature !== void 0 ? temperature.unit : formatReading({
|
|
50323
|
+
value: 0,
|
|
50324
|
+
quantity: "temperature"
|
|
50325
|
+
}).unit
|
|
50259
50326
|
}),
|
|
50260
50327
|
/* @__PURE__ */ jsx("span", {
|
|
50261
50328
|
className: "text-xs text-foreground-subtle ml-2 capitalize",
|
|
@@ -50276,12 +50343,12 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
50276
50343
|
style: { transform: `rotate(${slice.windBearing}deg)` }
|
|
50277
50344
|
}) : /* @__PURE__ */ jsx(Wind, { className: "h-3.5 w-3.5" }),
|
|
50278
50345
|
label: t("uiLibrary.weatherControl.wind", "Wind"),
|
|
50279
|
-
value:
|
|
50346
|
+
value: formatWeather(slice.windSpeed, "speed")
|
|
50280
50347
|
}),
|
|
50281
50348
|
slice.pressure !== null && /* @__PURE__ */ jsx(Readout$1, {
|
|
50282
50349
|
icon: /* @__PURE__ */ jsx(Gauge, { className: "h-3.5 w-3.5" }),
|
|
50283
50350
|
label: t("uiLibrary.weatherControl.pressure", "Pressure"),
|
|
50284
|
-
value:
|
|
50351
|
+
value: formatWeather(slice.pressure, "pressure")
|
|
50285
50352
|
})
|
|
50286
50353
|
]
|
|
50287
50354
|
})]
|
|
@@ -50520,6 +50587,16 @@ var VOLUME_MAX = 9;
|
|
|
50520
50587
|
/** Amber kibble accent for the bowl fill; destructive red when low. */
|
|
50521
50588
|
var KIBBLE = "#f59e0b";
|
|
50522
50589
|
var KIBBLE_LOW = "#ef4444";
|
|
50590
|
+
function desiccantLabel(t, seconds) {
|
|
50591
|
+
const rendered = formatReading({
|
|
50592
|
+
value: seconds,
|
|
50593
|
+
quantity: "duration"
|
|
50594
|
+
});
|
|
50595
|
+
return t("uiLibrary.petFeeder.desiccant", "{{text}}{{unit}} desiccant", {
|
|
50596
|
+
text: rendered.text,
|
|
50597
|
+
unit: rendered.unit
|
|
50598
|
+
});
|
|
50599
|
+
}
|
|
50523
50600
|
function clampPortion(grams) {
|
|
50524
50601
|
return Math.min(PET_FEEDER_MANUAL_FEED_MAX, Math.max(PET_FEEDER_MANUAL_FEED_MIN, grams));
|
|
50525
50602
|
}
|
|
@@ -50642,7 +50719,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
50642
50719
|
}),
|
|
50643
50720
|
display.desiccantLeftDays !== null && /* @__PURE__ */ jsx(Readout, {
|
|
50644
50721
|
icon: /* @__PURE__ */ jsx(Wind, { className: "h-3.5 w-3.5" }),
|
|
50645
|
-
label: t
|
|
50722
|
+
label: desiccantLabel(t, display.desiccantLeftDays)
|
|
50646
50723
|
}),
|
|
50647
50724
|
onBatteries && /* @__PURE__ */ jsxs("span", {
|
|
50648
50725
|
className: "inline-flex items-center gap-1 font-medium text-amber-500",
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Resolution order (device override → role default → live slice →
|
|
7
|
-
* ROLE_DESCRIPTOR canonicalUnit). `??` everywhere, never `||`: `precision: 0`
|
|
8
|
-
* is a legal override and must win over a slice `precision: 2`.
|
|
9
|
-
*
|
|
10
|
-
* Conversion happens at RENDER only — storage stays in source units. An
|
|
11
|
-
* override unit converts the raw slice value ONLY when it shares a dimension
|
|
12
|
-
* with the source unit (`canConvertUnit`); on refusal the display falls back to
|
|
13
|
-
* identity + the SOURCE unit so a unit label and its value math can never
|
|
14
|
-
* disagree. Precision applies AFTER conversion (by construction — the caller
|
|
15
|
-
* runs `formatNumeric(toDisplayValue(raw), precision)`).
|
|
2
|
+
* Shared resolution of a sensor's display string. The stored number is always
|
|
3
|
+
* the quantity's base unit (D944 §4). A display preference (default metric)
|
|
4
|
+
* or an operator override picks the label; the value is converted to match.
|
|
16
5
|
*/
|
|
17
|
-
import type { DeviceDisplayOverride, RoleDisplayDefault } from "@camstack/types";
|
|
6
|
+
import type { DeviceDisplayOverride, ReadingQuantity, RoleDisplayDefault } from "@camstack/types";
|
|
7
|
+
import { type DisplayPreference } from "@camstack/types";
|
|
18
8
|
export interface SensorDisplayInput {
|
|
19
9
|
/** Per-device override from the row (`DeviceInfo.display`). */
|
|
20
10
|
readonly display?: DeviceDisplayOverride;
|
|
@@ -22,20 +12,28 @@ export interface SensorDisplayInput {
|
|
|
22
12
|
readonly capName?: string;
|
|
23
13
|
/** Role default (`roleDisplayDefaults[device.role]`). */
|
|
24
14
|
readonly roleDefault?: RoleDisplayDefault;
|
|
25
|
-
/**
|
|
26
|
-
readonly
|
|
27
|
-
/**
|
|
28
|
-
readonly
|
|
29
|
-
/**
|
|
30
|
-
readonly
|
|
15
|
+
/** Quantity of the stored number. `device` reads `deviceQuantity`. */
|
|
16
|
+
readonly quantity?: ReadingQuantity;
|
|
17
|
+
/** The device's chosen quantity when `quantity` is `device`. */
|
|
18
|
+
readonly deviceQuantity?: ReadingQuantity;
|
|
19
|
+
/** Precision declared on the reading, when the operator has not overridden it. */
|
|
20
|
+
readonly declarationPrecision?: number;
|
|
21
|
+
/** Read-time preference. Defaults to metric. */
|
|
22
|
+
readonly preference?: DisplayPreference;
|
|
31
23
|
}
|
|
32
24
|
export interface ResolvedSensorDisplay {
|
|
33
|
-
/** Display unit — `''` when
|
|
25
|
+
/** Display unit — `''` when the quantity has none. */
|
|
34
26
|
readonly unit: string;
|
|
35
27
|
/** Display precision (int 0-10) or undefined for auto-format. */
|
|
36
28
|
readonly precision: number | undefined;
|
|
37
|
-
/** Convert a
|
|
38
|
-
* override unit differs from the source unit AND both share a dimension. */
|
|
29
|
+
/** Convert a base-unit value into the display unit. */
|
|
39
30
|
readonly toDisplayValue: (raw: number) => number;
|
|
31
|
+
/** Convert a display-unit value back into the stored base. */
|
|
32
|
+
readonly fromDisplayValue: (shown: number) => number;
|
|
33
|
+
/** Text and unit for one base-unit sample. */
|
|
34
|
+
readonly format: (raw: number) => {
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly unit: string;
|
|
37
|
+
};
|
|
40
38
|
}
|
|
41
39
|
export declare function resolveSensorDisplay(input: SensorDisplayInput): ResolvedSensorDisplay;
|