Dashboard Studio 1.14.0: new cards, kiosk announcements, guest mode, templates, sharing and phone app

- Seven new cards: parcels, air quality, water and gas usage, solar forecast, garage and gate, sports, Formula One
- Kiosk: announcements from automations, tab rotation, guest mode, click sound
- Editor: templates, widget presets, undo history, alignment guides, Lovelace import, share link and QR code, theme variants, suggestions, touch and hold actions
- Phones: installable app with shortcuts, offline states, tab swipe, pull to refresh, bottom sheets, haptics, phone/large visibility, landscape layout, editing on the phone, away tab
- Fixed: the weather forecast and to-do list could stay on Loading…

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 21:08:42 +01:00
parent 93bb42dcbb
commit 79d4a0f011
17 changed files with 2256 additions and 58 deletions
+104 -3
View File
@@ -133,6 +133,8 @@ class MockHA:
entity("sensor.model_y_odometer", "24816", friendly_name="Model Y Odometer", unit_of_measurement="km", device_class="distance"),
]
}
self.states.update({e["entity_id"]: e for e in self._extra_entities()})
self.states["cover.garage_door"]["context"] = {"id": "mock", "user_id": "mock-user-alex"}
self.clients = set()
self.todo = [{"uid": "1", "summary": "Milk", "status": "needs_action"}, {"uid": "2", "summary": "Bread", "status": "needs_action"},
{"uid": "3", "summary": "Coffee beans", "status": "needs_action"}, {"uid": "4", "summary": "Batteries", "status": "completed"}]
@@ -143,6 +145,94 @@ class MockHA:
}
self.note_subs = set()
def _extra_entities(self):
"""Parcels, air quality, meters, solar forecast, sports and Formula One."""
now = datetime.now().astimezone()
day = now.replace(hour=0, minute=0, second=0, microsecond=0)
at = lambda d, h, m=0: (day + timedelta(days=d, hours=h, minutes=m)).isoformat()
drivers = [("Max", "Verstappen", "VER", "Red Bull", 395), ("Lando", "Norris", "NOR", "McLaren", 372), ("Oscar", "Piastri", "PIA", "McLaren", 341),
("Charles", "Leclerc", "LEC", "Ferrari", 290), ("George", "Russell", "RUS", "Mercedes", 245), ("Lewis", "Hamilton", "HAM", "Ferrari", 198)]
standings = [{"position": str(i + 1), "points": str(p), "wins": str(max(0, 6 - i * 2)), "Driver": {"givenName": g, "familyName": f, "code": c}, "Constructors": [{"name": team}]}
for i, (g, f, c, team, p) in enumerate(drivers)]
teams = [{"position": str(i + 1), "points": str(p), "wins": str(w), "Constructor": {"name": n}} for i, (n, p, w) in enumerate([("McLaren", 713, 9), ("Ferrari", 488, 3), ("Red Bull", 470, 7), ("Mercedes", 402, 2)])]
results = [{"position": str(i + 1), "points": str([25, 18, 15, 12, 10, 8][i]), "Driver": {"givenName": g, "familyName": f, "code": c}, "Constructor": {"name": team},
"status": "Finished", "Time": {"time": ["1:32:10.553", "+2.411", "+8.902", "+15.300", "+21.774", "+30.105"][i]}}
for i, (g, f, c, team, _) in enumerate(drivers)]
return [
entity("sensor.postnl_delivery", "2", friendly_name="PostNL Delivery",
enroute=[{"key": "3SABCD123", "name": "Bol.com", "status_message": "De bezorger is onderweg", "planned_date": at(0, 14), "planned_to": at(0, 16), "url": "https://jouw.postnl.nl/"},
{"key": "3SXYZ987", "name": "Coolblue", "status_message": "Wordt gesorteerd", "planned_date": at(1, 0)}],
delivered=[{"key": "3SQQQ555", "name": "Zalando", "status_message": "Bezorgd", "delivery_date": at(-1, 11, 20)}]),
entity("sensor.living_room_co2", "940", friendly_name="Living Room CO2", unit_of_measurement="ppm", device_class="carbon_dioxide"),
entity("sensor.living_room_pm25", "8", friendly_name="Living Room PM2.5", unit_of_measurement="µg/m³", device_class="pm25"),
entity("sensor.bedroom_co2", "1340", friendly_name="Bedroom CO2", unit_of_measurement="ppm", device_class="carbon_dioxide"),
entity("sensor.bedroom_humidity", "72", friendly_name="Bedroom Humidity", unit_of_measurement="%", device_class="humidity"),
entity("sensor.office_co2", "610", friendly_name="Office CO2", unit_of_measurement="ppm", device_class="carbon_dioxide"),
entity("sensor.office_humidity", "46", friendly_name="Office Humidity", unit_of_measurement="%", device_class="humidity"),
entity("sensor.water_meter", "1234.567", friendly_name="Water Meter", unit_of_measurement="m³", device_class="water", state_class="total_increasing"),
entity("sensor.gas_meter", "4321.12", friendly_name="Gas Meter", unit_of_measurement="m³", device_class="gas", state_class="total_increasing"),
entity("sensor.energy_production_today", "18.4", friendly_name="Estimated energy production - today", unit_of_measurement="kWh", device_class="energy"),
entity("sensor.energy_production_tomorrow", "12.1", friendly_name="Estimated energy production - tomorrow", unit_of_measurement="kWh", device_class="energy"),
entity("sensor.power_production_now", "1240", friendly_name="Estimated power production - now", unit_of_measurement="W", device_class="power"),
entity("sensor.team_tracker_ajax", "PRE", friendly_name="Ajax", sport="soccer", league="Eredivisie", team_name="Ajax", team_abbr="AJA", team_homeaway="home",
opponent_name="PSV", opponent_abbr="PSV", date=at(0, 20), kickoff_in="in a few hours", venue="Johan Cruijff ArenA", tv_network="ESPN"),
entity("sensor.f1_next_race", at(5, 18), friendly_name="F1 Next Race", season="2026", round="21", race_name="São Paulo Grand Prix",
circuit_name="Autódromo José Carlos Pace", circuit_locality="São Paulo", circuit_country="Brazil",
first_practice_start=at(3, 15, 30), sprint_qualifying_start=at(3, 19, 30), sprint_start=at(4, 15), qualifying_start=at(4, 19), race_start=at(5, 18)),
entity("sensor.f1_driver_standings", "Max Verstappen", friendly_name="F1 Driver Standings", driver_standings=standings),
entity("sensor.f1_constructor_standings", "McLaren", friendly_name="F1 Constructor Standings", constructor_standings=teams),
entity("sensor.f1_last_race_results", "Mexico City Grand Prix", friendly_name="F1 Last Race Results", race_name="Mexico City Grand Prix", results=results),
]
def statistics(self, ids, start):
start_dt = datetime.fromisoformat(start.replace("Z", "+00:00")).astimezone()
now = datetime.now().astimezone()
out = {}
for eid in ids:
base = {"sensor.gas_meter": 3.2}.get(eid, 0.18)
rows, d = [], start_dt
while d <= now:
seed = (d.toordinal() * 7 + len(eid)) % 10
change = base * (0.6 + seed / 12)
if d.date() == now.date():
change *= now.hour / 24
rows.append({"start": int(d.timestamp() * 1000), "end": int((d + timedelta(days=1)).timestamp() * 1000), "change": round(change, 3)})
d += timedelta(days=1)
out[eid] = rows
return out
def solar_forecast(self):
day = datetime.now().astimezone().replace(hour=0, minute=0, second=0, microsecond=0)
hours = {}
for d, peak in ((0, 2600), (1, 1700)):
for h in range(6, 21):
wh = max(0, peak * math.sin(math.pi * (h - 6) / 14))
hours[(day + timedelta(days=d, hours=h)).isoformat()] = round(wh)
return {"mock_entry": {"wh_hours": hours}}
def broadcast_event(self, event_type, data):
event = {"event_type": event_type, "data": data or {}, "time_fired": now_iso()}
for ws, sub_ids in list(self.clients):
for sid in sub_ids:
asyncio.ensure_future(self._safe_send(ws, {"id": sid, "type": "event", "event": event}))
LOVELACE = {"views": [
{"title": "Home", "path": "home", "icon": "mdi:home", "cards": [
{"type": "entities", "title": "Living room", "entities": ["light.living_room", "light.reading_lamp", {"entity": "sensor.living_room_temperature", "name": "Temperature"}]},
{"type": "thermostat", "entity": "climate.thermostat"},
{"type": "weather-forecast", "entity": "weather.home"},
{"type": "horizontal-stack", "cards": [{"type": "button", "entity": "switch.coffee_machine"}, {"type": "light", "entity": "light.kitchen"}]},
{"type": "media-control", "entity": "media_player.living_room_speaker"},
{"type": "markdown", "content": "## Welcome home"},
]},
{"title": "Energy", "path": "energy", "cards": [
{"type": "gauge", "entity": "sensor.power_usage", "min": 0, "max": 5000},
{"type": "history-graph", "entities": ["sensor.power_usage", "sensor.solar_production"], "hours_to_show": 24},
{"type": "tile", "entity": "sensor.solar_production"},
{"type": "picture-entity", "entity": "camera.front_yard"},
]},
]}
# -- state handling ---------------------------------------------------
def set_state(self, entity_id, state=None, **attrs):
@@ -454,10 +544,10 @@ class MockHA:
]})
elif typ == "config/entity_registry/list":
areas = {
"living_room": ["light.living_room", "light.reading_lamp", "light.downstairs", "media_player.living_room_speaker", "media_player.tv", "sensor.living_room_temperature", "sensor.living_room_humidity", "binary_sensor.hallway_motion", "climate.thermostat"],
"living_room": ["sensor.living_room_co2", "sensor.living_room_pm25", "light.living_room", "light.reading_lamp", "light.downstairs", "media_player.living_room_speaker", "media_player.tv", "sensor.living_room_temperature", "sensor.living_room_humidity", "binary_sensor.hallway_motion", "climate.thermostat"],
"kitchen": ["light.kitchen", "switch.coffee_machine", "media_player.kitchen_speaker", "sensor.washer_status"],
"bedroom": ["light.bedroom", "binary_sensor.front_door"],
"office": ["fan.office", "input_boolean.guest_mode"],
"bedroom": ["light.bedroom", "binary_sensor.front_door", "sensor.bedroom_co2", "sensor.bedroom_humidity"],
"office": ["fan.office", "input_boolean.guest_mode", "sensor.office_co2", "sensor.office_humidity"],
"garage": ["cover.garage_door", "lock.front_door"],
}
rows = [{"entity_id": e, "area_id": a, "device_id": None, "disabled_by": None, "hidden_by": None, "entity_category": None} for a, es in areas.items() for e in es]
@@ -466,6 +556,17 @@ class MockHA:
await ws.send_json({"id": mid, "type": "result", "success": True, "result": rows})
elif typ == "config/device_registry/list":
await ws.send_json({"id": mid, "type": "result", "success": True, "result": []})
elif typ == "recorder/statistics_during_period":
await ws.send_json({"id": mid, "type": "result", "success": True, "result": self.statistics(req.get("statistic_ids") or [], req.get("start_time"))})
elif typ == "energy/solar_forecast":
await ws.send_json({"id": mid, "type": "result", "success": True, "result": self.solar_forecast()})
elif typ == "fire_event":
self.broadcast_event(req.get("event_type"), req.get("event_data"))
await ws.send_json({"id": mid, "type": "result", "success": True, "result": {"context": {"id": "mock"}}})
elif typ == "lovelace/dashboards/list":
await ws.send_json({"id": mid, "type": "result", "success": True, "result": [{"url_path": "wall-tablet", "title": "Wall tablet", "mode": "storage", "icon": "mdi:tablet"}]})
elif typ == "lovelace/config":
await ws.send_json({"id": mid, "type": "result", "success": True, "result": self.LOVELACE})
elif typ == "ping":
await ws.send_json({"id": mid, "type": "pong"})
else: