Dashboard Studio 1.8.0: vacuum card, clock position and background, light groups

- Vacuum card: status, battery, start/pause, dock, locate, suction
  power and an optional live map from a camera or image entity
- Clock: 3x3 text position picker and a background image from links or
  Immich albums (shared slideshow code with the Photos card), dimmable
- Light card lists the lights of a light group with their own switches;
  picking a group grows the card to fit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 23:37:23 +01:00
parent 75ab27db10
commit e73c688017
7 changed files with 211 additions and 50 deletions
+35 -2
View File
@@ -13,10 +13,10 @@ def now_iso():
return datetime.now(timezone.utc).isoformat()
def entity(entity_id, state, **attrs):
def entity(eid, state, /, **attrs):
ts = now_iso()
return {
"entity_id": entity_id,
"entity_id": eid,
"state": state,
"attributes": attrs,
"last_changed": ts,
@@ -47,6 +47,10 @@ class MockHA:
entity("light.reading_lamp", "on", friendly_name="Reading Lamp", brightness=140, rgb_color=[255, 140, 60], supported_color_modes=["rgb"]),
entity("light.kitchen", "off", friendly_name="Kitchen", brightness=None, supported_color_modes=["brightness"]),
entity("light.bedroom", "on", friendly_name="Bedroom", brightness=90, supported_color_modes=["brightness"]),
entity("light.downstairs", "on", friendly_name="Downstairs", brightness=160, supported_color_modes=["brightness"],
entity_id=["light.living_room", "light.kitchen", "light.reading_lamp"]),
entity("vacuum.robot", "docked", friendly_name="Robot Vacuum", battery_level=82, fan_speed="standard",
fan_speed_list=["quiet", "standard", "turbo", "max"], status="Charging"),
entity("switch.coffee_machine", "off", friendly_name="Coffee Machine"),
entity("fan.office", "off", friendly_name="Office Fan"),
entity("sensor.living_room_temperature", "21.4", friendly_name="Living Room Temperature", unit_of_measurement="°C", device_class="temperature"),
@@ -104,6 +108,8 @@ class MockHA:
new["attributes"].update(attrs)
new["last_updated"] = now_iso()
self.states[entity_id] = new
if entity_id.startswith("light.") and "entity_id" not in new["attributes"]:
self._sync_groups(entity_id)
event = {"event_type": "state_changed", "data": {"entity_id": entity_id, "old_state": old, "new_state": new}, "time_fired": now_iso()}
for ws, sub_ids in list(self.clients):
for sid in sub_ids:
@@ -115,13 +121,40 @@ class MockHA:
except Exception: # client went away
pass
def _vacuum(self, eid, service, data):
if service == "start":
self.set_state(eid, "cleaning", status="Cleaning")
elif service == "pause":
self.set_state(eid, "paused", status="Paused")
elif service in ("return_to_base", "stop"):
self.set_state(eid, "returning", status="Returning home")
asyncio.get_event_loop().call_later(6, lambda: self.set_state(eid, "docked", status="Charging"))
elif service == "set_fan_speed":
self.set_state(eid, None, fan_speed=data.get("fan_speed"))
def _sync_groups(self, changed):
for gid, g in list(self.states.items()):
members = g["attributes"].get("entity_id")
if not gid.startswith("light.") or not isinstance(members, list) or changed not in members:
continue
on = [self.states[m] for m in members if m in self.states and self.states[m]["state"] == "on"]
bright = round(sum(m["attributes"].get("brightness") or 0 for m in on) / len(on)) if on else None
self.set_state(gid, "on" if on else "off", brightness=bright)
def call_service(self, domain, service, data):
targets = data.get("entity_id") or (data.get("target") or {}).get("entity_id") or []
if isinstance(targets, str):
targets = [targets]
expanded = []
for eid in targets:
members = (self.states.get(eid) or {}).get("attributes", {}).get("entity_id")
expanded += members if eid.startswith("light.") and isinstance(members, list) else [eid]
for eid in expanded:
if eid not in self.states:
continue
if domain == "vacuum" or eid.startswith("vacuum."):
self._vacuum(eid, service, data)
continue
st = self.states[eid]["state"]
if eid.startswith("climate.") and service in ("turn_on", "turn_off", "toggle"):
on = service == "turn_on" or (service == "toggle" and st == "off")