Add a colour-wheel widget to the dashboard
A custom hue ring — a conic gradient, not a canvas — with saturation and brightness sliders beside or under it depending on the tile's shape, sized for a wall panel and reachable from a keyboard. It publishes [h, s, v] by default, which is what the reference installation's DMX encoders read, and `format` switches that to [r, g, b] or "#rrggbb". `usePublish` moves to its own module so a widget in a file of its own can reach it without importing `widgets.tsx` back.
This commit is contained in:
+79
-2
@@ -15,7 +15,7 @@ What it builds::
|
||||
|
||||
The panel is a small-house energy and climate display — the thing a person
|
||||
would actually hang in a hallway — and it is also the widget gallery: all
|
||||
fifteen types are on it, including the variants no editor field can write
|
||||
sixteen types are on it, including the variants no editor field can write
|
||||
(a switch drawn as a button, a segmented dropdown, a bar with a nested
|
||||
reading, an icon ladder with labels, fixed chart axes, decimal places).
|
||||
|
||||
@@ -236,6 +236,41 @@ def process(day_start, climate=None):
|
||||
return {"forecast": forecast, "agenda": agenda}
|
||||
'''
|
||||
|
||||
LAMP_SOURCE = '''"""What the hallway lamp was set to, said in words.
|
||||
|
||||
The wheel on the panel publishes ``[h, s, v]`` — hue in degrees, saturation and
|
||||
value in percent, which is what a DMX encoder divides down. A real installation
|
||||
puts an MQTT or Art-Net node here; this only names the colour, so the panel can
|
||||
show that the control reached something.
|
||||
"""
|
||||
|
||||
#: Where each name starts, in degrees. Read from the bottom up, so the last row
|
||||
#: a hue has passed wins — and red is at both ends, because the wheel wraps.
|
||||
NAMES = [
|
||||
(0, "Red"),
|
||||
(15, "Amber"),
|
||||
(45, "Yellow"),
|
||||
(70, "Lime"),
|
||||
(100, "Green"),
|
||||
(160, "Cyan"),
|
||||
(200, "Blue"),
|
||||
(260, "Violet"),
|
||||
(290, "Magenta"),
|
||||
(320, "Pink"),
|
||||
(350, "Red"),
|
||||
]
|
||||
|
||||
|
||||
def process(light_color):
|
||||
hue, saturation, value = (list(light_color) + [0, 0, 0])[:3]
|
||||
if value < 2:
|
||||
return {"light_state": "Off"}
|
||||
if saturation < 10:
|
||||
return {"light_state": "White at {:.0f}%".format(value)}
|
||||
name = next(label for at, label in reversed(NAMES) if hue >= at)
|
||||
return {"light_state": "{} at {:.0f}%".format(name, value)}
|
||||
'''
|
||||
|
||||
HOUSE_INPUTS = [
|
||||
# What the controls on the panel write to. The initial values are what the
|
||||
# dashboard reads back before anyone touches anything, which is why the
|
||||
@@ -244,6 +279,12 @@ HOUSE_INPUTS = [
|
||||
{"spec": {"name": "away", "dtype": "bool"}, "initial": False},
|
||||
{"spec": {"name": "setpoint", "dtype": "float"}, "initial": 21.0},
|
||||
{"spec": {"name": "tariff", "dtype": "float"}, "initial": 32.0},
|
||||
# Three numbers rather than a record: [hue, saturation, value], which is
|
||||
# what the colour wheel sends and what a DMX encoder reads.
|
||||
{
|
||||
"spec": {"name": "light_color", "dtype": "list", "item": "float"},
|
||||
"initial": [38, 72, 80],
|
||||
},
|
||||
# False rather than nothing: a declared message with no starting value is
|
||||
# a node that can never run, and the engine says so on the canvas.
|
||||
{"spec": {"name": "boost", "dtype": "bool"}, "initial": False},
|
||||
@@ -368,6 +409,13 @@ HOUSE_NODES = [
|
||||
"requires": [{"name": "boost", "dtype": "bool"}],
|
||||
"provides": [{"name": "hot_water", "dtype": "bool"}],
|
||||
},
|
||||
{
|
||||
"id": "lamp",
|
||||
"type": "python",
|
||||
"title": "Name the lamp's colour",
|
||||
"requires": [{"name": "light_color", "dtype": "list", "item": "float"}],
|
||||
"provides": [{"name": "light_state", "dtype": "str"}],
|
||||
},
|
||||
{
|
||||
"id": "water_label",
|
||||
"type": "change",
|
||||
@@ -961,6 +1009,30 @@ ENERGY_WIDGETS = [
|
||||
"label": "Boost 15 min",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "mood",
|
||||
"type": "color",
|
||||
"title": "Hallway lamp",
|
||||
# Wider than it is tall, which is what the wheel lays itself out for:
|
||||
# the ring takes the row's height and the two components sit beside it.
|
||||
"layout": at(0, 6, 11, 2),
|
||||
"config": {
|
||||
"target": msg(HOUSE, "light_color"),
|
||||
"dtype": "list",
|
||||
# The default, said out loud: [h, s, v] as a DMX encoder reads it.
|
||||
# "rgb" sends three 0-255 channels, "hex" a "#rrggbb" string.
|
||||
"format": "hsv",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "lamp",
|
||||
"type": "stat",
|
||||
"title": "Lamp",
|
||||
"layout": at(11, 6, 5, 2),
|
||||
# What the flow made of the colour — the proof that the wheel reached
|
||||
# something, the way the hot-water button has its own state beside it.
|
||||
"config": {"message": msg(HOUSE, "light_state"), "dtype": "str"},
|
||||
},
|
||||
]
|
||||
|
||||
MODEL_WIDGETS = [
|
||||
@@ -1249,7 +1321,12 @@ def main() -> int:
|
||||
HOUSE,
|
||||
"House",
|
||||
HOUSE_NODES,
|
||||
{"house": HOUSE_SOURCE, "meter": METER_SOURCE, "plan": PLAN_SOURCE},
|
||||
{
|
||||
"house": HOUSE_SOURCE,
|
||||
"meter": METER_SOURCE,
|
||||
"plan": PLAN_SOURCE,
|
||||
"lamp": LAMP_SOURCE,
|
||||
},
|
||||
inputs=HOUSE_INPUTS,
|
||||
)
|
||||
seed_flow(
|
||||
|
||||
Reference in New Issue
Block a user