Counter widget: one to three linked counters are the digits of one number, published on one message in the finest counter's unit (hours and minutes send 07:30 as 450, 07:55 + 5 min carries to 08:00); wraps or stops at the coarsest's min/max. The arithmetic is one pure module, ui/core/counter.ts, with counter.check.ts. A tile steps each counter with a press up and down and publishes once 300 ms after the last; a round face turns the selected counter on the ring (wheel, keys, a drag counted in steps), its arc where that counter stands in its turn, and a press on a counter's digits selects it. Default config is the time picker. Editor rows, widgetIssue, server type and dtype, docs

This commit is contained in:
Melvin Strobl
2026-10-16 08:58:42 +02:00
parent 4ce50b5c3a
commit 21c7961d49
16 changed files with 745 additions and 33 deletions
+17
View File
@@ -48,6 +48,10 @@ RUN_LINES = 5
#: legible stacked in one tile.
BAR_ROWS = 8
#: How many counters one counter widget links into its number. Mirrored in the
#: client (``ui/core/counter.ts``, ``MAX_COUNTERS``).
COUNTERS = 3
#: Resolved out here on purpose: the store has a ``list`` method, which
#: shadows the builtin for any annotation written inside the class.
Bindings = list[dict[str, Any]]
@@ -75,6 +79,7 @@ WidgetType = Literal[
"input",
"dropdown",
"color",
"counter",
]
#: Widgets whose only binding is the message they publish. A player is not one:
@@ -88,6 +93,7 @@ INPUT_WIDGETS = {
"input",
"dropdown",
"color",
"counter",
}
#: What a colour widget puts on the wire, by the format it was configured for.
@@ -108,6 +114,8 @@ WIDGET_DTYPES: dict[str, set[str]] = {
# answer and a `record` request instead, checked separately below.
"chart": {"float", "int"},
"slider": {"float", "int"},
# One number, the digits of which its counters are.
"counter": {"float", "int"},
"switch": {"bool"},
"agenda": {"list"},
"notification": {"record"},
@@ -229,6 +237,12 @@ class WidgetDef(BaseModel):
A slider may also show what it is setting: ``reading`` binds
``{message, dtype, label}`` — a room's temperature beside its setpoint —
held to the same numbers its target is.
A counter publishes one number to its target, of which its ``counters``
are the digits: up to three ``{label, step, size, min, max}``, coarsest
first, so hours and minutes publish 07:30 as 450. ``wrap`` says whether it
wraps round at its ends; ``docs/interface/dashboards.md`` has the
arithmetic.
"""
id: str
@@ -397,6 +411,9 @@ class WidgetDef(BaseModel):
if self.type == "bar" and len(self.bar_rows) > BAR_ROWS:
raise ValueError(f"a bar draws at most {BAR_ROWS} readings")
if self.type == "counter" and len(self.config.get("counters") or []) > COUNTERS:
raise ValueError(f"a counter links at most {COUNTERS} counters")
if self.type == "color":
# The row above allows both shapes a colour travels as; the format
# is what decides which of them this widget actually sends. An