"""Three screens on one panel: the house, its comfort, and its electricity. One dashboard each rather than three pages of one, because a panel carries several whole dashboards and switches between them on a rail — which is the thing that exists, and pages are not. Every control binds to the message an arbiter both reads and writes, so a tile shows what actually reached the fixture and setting it is what overrides the automation. There is no second tile saying what the first one really did. """ from __future__ import annotations from typing import Any #: A ten-inch panel: 1280x800, twelve columns of 96px, twelve rows of 51px. COLUMNS = 12 CANVAS = (1280, 800) ROWS = 12 def _at(x: int, y: int, w: int, h: int) -> dict[str, Any]: return {"lg": {"x": x, "y": y, "w": w, "h": h}} def stat(id_, title, message, unit="", precision=1, dtype="float", **at): return { "id": id_, "type": "stat", "title": title, "layout": _at(**at), "config": { "message": message, "dtype": dtype, "unit": unit, "precision": precision, }, } def switch(id_, title, target, **at): return { "id": id_, "type": "switch", "title": title, "layout": _at(**at), "config": {"target": target, "dtype": "bool", "style": "button"}, } def button(id_, title, target, value, **at): return { "id": id_, "type": "button", "title": title, "layout": _at(**at), "config": {"target": target, "dtype": "str", "value": value, "label": title}, } def slider(id_, title, target, lo, hi, step=1, unit="", **at): return { "id": id_, "type": "slider", "title": title, "layout": _at(**at), "config": { "target": target, "dtype": "float", "min": lo, "max": hi, "step": step, "unit": unit, }, } def gauge(id_, title, message, lo, hi, unit="", **at): return { "id": id_, "type": "gauge", "title": title, "layout": _at(**at), "config": { "message": message, "dtype": "float", "min": lo, "max": hi, "unit": unit, "precision": 0, }, } def chart(id_, title, request, series, unit="", **at): return { "id": id_, "type": "chart", "title": title, "layout": _at(**at), "config": { "source": "query", "request": request, "request_dtype": "record", "message": series, "dtype": "series", "range_s": 21600, "refresh_s": 300, "unit": unit, }, } def icon(id_, title, message, rules, dtype="int", **at): return { "id": id_, "type": "icon", "title": title, "layout": _at(**at), "config": {"message": message, "dtype": dtype, "rules": rules}, } def dropdown(id_, title, target, options, **at): return { "id": id_, "type": "dropdown", "title": title, "layout": _at(**at), "config": { "target": target, "dtype": "str", "style": "segmented", "options": [{"label": label, "value": value} for label, value in options], }, } def cover(id_, title, target, labels, **at): """A motor, as one control that both moves it and says where it is. Three buttons could only ever publish; this reads back as well, because the motor writes what it is doing to the same message the control sets — so the segment that is held is the direction it actually went. """ up, stop, down = labels return dropdown( id_, title, target, [(up, "UP"), (stop, "STOP"), (down, "DOWN")], **at, ) # Three screens, each on the 1280x800 of a ten-inch panel: twelve columns of # 96px and twelve rows of 51px. Nothing runs past the bottom, because a panel # does not scroll — what does not fit is not on the screen. HOME = [ # The top band is three rows because the status icon draws a glyph *and* a # word, and in two the word falls off the bottom of the tile. stat("indoor", "Inside", "climate.indoor", "°C", 1, x=0, y=0, w=3, h=3), stat("outdoor", "Outside", "weather.outdoor_temp", "°C", 1, x=3, y=0, w=3, h=3), stat("humidity", "Humidity", "weather.indoor_hum", "%", 0, x=6, y=0, w=3, h=3), icon( "power_state", "Power", "power.watch", [ {"at": 0, "icon": "check", "color": "success", "label": "Normal"}, {"at": 1, "icon": "triangle-alert", "color": "warning", "label": "Watch"}, {"at": 3, "icon": "zap-off", "color": "danger", "label": "Mains"}, ], x=9, y=0, w=3, h=3, ), dropdown( "scene", "Scene", "lights.scene", [ ("Off", "off"), ("Day", "day"), ("Night", "night"), ("Sleep", "sleep"), ("Outside", "outside"), ], x=0, y=3, w=6, h=3, ), # Three rows here too: a slider is a number, a track and its tick labels. slider( "brightness", "Brightness", "lights.brightness", 0, 100, 5, "%", x=6, y=3, w=6, h=3, ), switch( "appliances", "Appliances", "appliances.appliances_manual", x=0, y=6, w=3, h=2 ), switch("bath", "Bathroom plug", "plugs.bath_manual", x=3, y=6, w=3, h=2), stat( "floor", "Floor heating", "oven.floor_heating", dtype="bool", precision=0, x=6, y=6, w=3, h=2, ), stat( "presence", "Who is in", "presence.state", dtype="str", precision=0, x=9, y=6, w=3, h=2, ), cover( "door", "Door", "shutters.door_shutter_cmd", ("Up", "Stop", "Down"), x=0, y=8, w=6, h=2, ), cover( "bed", "Bed", "shutters.bed_shutter_cmd", ("Up", "Stop", "Down"), x=6, y=8, w=6, h=2, ), { "id": "load", "type": "bar", "title": "House draw", "layout": _at(x=0, y=10, w=4, h=2), "config": { "message": "power.out_w", "min": 0, "max": 3000, "unit": "W", "precision": 0, # How much of what the house is drawing came off the roof. "inner": [{"message": "power.pv_w", "dtype": "float", "label": "Solar"}], }, }, # A bar rather than a gauge: two rows is not enough arc to read across a # room, and the same number in a bar is. The gauge is on Energy, where it # has the height for one. { "id": "soc", "type": "bar", "title": "Battery", "layout": _at(x=4, y=10, w=4, h=2), "config": { "message": "power.soc", "min": 0, "max": 100, "unit": "%", "precision": 0, }, }, { "id": "alert", "type": "notification", "title": "What is happening", "layout": _at(x=8, y=10, w=4, h=2), "config": {"message": "power.alert"}, }, ] COMFORT = [ { "id": "forecast", "type": "forecast", "title": "The next few days", "layout": _at(x=0, y=0, w=6, h=3), "config": {"message": "weather.forecast", "count": 4}, }, { "id": "agenda", "type": "agenda", "title": "Coming up", "layout": _at(x=6, y=0, w=6, h=3), "config": {"message": "calendar.events", "count": 4}, }, slider("preset", "Wanted", "climate.preset", 16, 26, 0.5, "°C", x=0, y=3, w=4, h=3), stat("band", "Up to", "climate.t_max", "°C", 1, x=4, y=3, w=2, h=3), switch("ac", "Heat pump", "hvac.enabled", x=6, y=3, w=3, h=3), switch("oven", "Pellet stove", "oven.oven_manual", x=9, y=3, w=3, h=3), cover( "window", "Window", "window.window_manual", ("Close", "Stop", "Open"), x=0, y=6, w=4, h=3, ), cover( "canopy", "Awning", "canopy.canopy_manual", ("In", "Stop", "Out"), x=4, y=6, w=4, h=3, ), switch("outdoor", "Outdoor plug", "outdoor.outdoor_manual", x=8, y=6, w=4, h=3), # Why each of them is doing what it is doing. A sentence needs the room, # and this is the screen where "off" without a reason is the annoying bit. stat( "hvac_why", "Heat pump", "hvac.why", dtype="str", precision=0, x=0, y=9, w=3, h=3, ), stat("oven_why", "Stove", "oven.why", dtype="str", precision=0, x=3, y=9, w=3, h=3), stat( "window_why", "Window", "window.why", dtype="str", precision=0, x=6, y=9, w=3, h=3, ), stat( "canopy_why", "Awning", "canopy.why", dtype="str", precision=0, x=9, y=9, w=3, h=3, ), ] # The three charts share a screen, so the history is read in one place and the # live figures above it say what "now" is on the same scales. # A chart spends about eighty pixels on its title, its range picker and its # legend whatever height it is given, so on an 800px panel two of them read and # three do not. The temperature history is the one that went; `history` still # answers for it, so it is a tile away if something else here is worth less. ENERGY = [ gauge("pv", "Solar", "power.pv_w", 0, 3000, "W", x=0, y=0, w=3, h=3), gauge("draw", "House", "power.out_w", 0, 3000, "W", x=3, y=0, w=3, h=3), gauge("grid", "Mains", "power.in_v", 0, 260, "V", x=6, y=0, w=3, h=3), gauge("charge", "Battery", "power.soc", 0, 100, "%", x=9, y=0, w=3, h=3), chart( "power_chart", "Where the power went", "history.power_request", "history.power_series", "W", x=0, y=3, w=12, h=4, ), chart( "battery_chart", "State of charge", "history.battery_request", "history.battery_series", "%", x=0, y=7, w=12, h=5, ), ] SCREENS = ( ("home", "Home", "house", HOME), ("comfort", "Comfort", "thermometer", COMFORT), ("energy", "Energy", "zap", ENERGY), )