Choose vacuum, vac & mop or mop on the vacuum card, and a climate card for the CYD (0.2.46)

App 0.2.46 / firmware 0.2.39:
- The vacuum's state message carries extras from its own device: the cleaning mode and water
  selects (found by translation key, then entity id ending, so localized ids work), the suction
  speeds to offer, the battery sensor (HA 2026.8 dropped battery_level), charging and the current
  room. Speeds and water levels a mode covers move into the mode row; Custom only while in use.
  The manager watches these entities and resends only the vacuum's message when one changes.
- Vacuum card redesign on both boards: robot with state, battery, charging bolt and room; Start
  (Pause, Resume) and Dock; then segmented rows for mode, suction and water, showing only the rows
  the chosen mode uses. Taps show at once and wait for Home Assistant; Locate is a round button in
  the Guition hero.
- CYD climate card without the arc: big setpoint between large -/+ keys (debounced, hold to
  repeat) and one row of mode keys in HA's order and colours, with a more key for fan and swing.
- Both profiles clear climate_edit_pending when the climate card is closed, instead of running
  ui_refresh every second while the flag stayed set.

Tested with the owner's Roborock S8 on HA 2026.9.1 (live data, OTA-flashed bench boards) and with
host renders of both boards; see docs/TEST_RESULTS_0246.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-15 15:22:28 +02:00
parent 4ff0d44312
commit aa2137f3f8
23 changed files with 1675 additions and 655 deletions
+24 -2
View File
@@ -429,6 +429,25 @@ class Manager:
self._registry_source, self._registry_index = registry, {item['entity_id']: item for item in registry}
return self._registry_index
def device_entries(self, entity):
"""Registry entries on the device of `entity` (itself included); the index follows the registry object."""
registry = getattr(self.ha, 'registry', [])
if getattr(self, '_devices_source', None) is not registry:
by_device = {}
for item in registry:
if item.get('device_id'):
by_device.setdefault(item['device_id'], []).append(item)
self._devices_source, self._by_device = registry, by_device
device = self.registry_index().get(entity, {}).get('device_id')
return self._by_device.get(device, []) if device else []
def related_entities(self, tile):
"""Entities a card reads besides its own: a vacuum's cleaning mode and water selects and its battery sensor."""
if not tile['entity'].startswith('vacuum.'):
return ()
from core import vacuum_related
return tuple(vacuum_related(tile['entity'], self.device_entries(tile['entity']), self.ha.states).values())
def header_message(self, layout):
return header_bar.message(layout, self.ha.states, self.registry_index(), getattr(self.ha, 'units', {}), getattr(self.ha, 'time_zone', None))
@@ -518,6 +537,7 @@ class Manager:
watched = {tile['entity'] for layout in self.layouts.values() for tile in layout['tiles']}
watched |= {item['entity'] for layout in self.layouts.values() for item in header_items(layout) if item['type'] == 'entity'}
watched |= {item['entity_id'] for item in self.screen_registry()}
watched |= {eid for layout in self.layouts.values() for tile in layout['tiles'] for eid in self.related_entities(tile)}
self._watched_key, self._watched = key, watched
return set(self._watched)
@@ -541,7 +561,9 @@ class Manager:
forecast=await self.cached(self.forecasts, tile['entity'], FORECAST_SECONDS, lambda: self.ha.forecast(tile['entity']))
# Hourly forecasts feed the weather card's next-hours strip (0.2.23+); refreshed every half hour.
hourly=await self.cached(self.forecasts, (tile['entity'],'hourly'), FORECAST_SECONDS, lambda: self.ha.forecast(tile['entity'],'hourly'))
message=state_message(index,tile,self.ha.states,extras(tile,self.ha.states,forecast,getattr(self.ha,'time_zone',None),hourly))
# A vacuum's card also reads selects and the battery sensor of its device (app 0.2.46).
device=self.device_entries(tile['entity']) if tile['entity'].startswith('vacuum.') else None
message=state_message(index,tile,self.ha.states,extras(tile,self.ha.states,forecast,getattr(self.ha,'time_zone',None),hourly,device=device))
if tile['entity'].startswith('sensor.') and hasattr(self.ha,'history'):
hours=tile.get('options',{}).get('history_hours',24)
entry=self.histories.get((tile['entity'],hours))
@@ -593,7 +615,7 @@ class Manager:
header_msg = previous['header']
states = []
for i, tile in enumerate(tiles):
reuse = not full and i < len(previous['states']) and tile['entity'] not in dirty
reuse = not full and i < len(previous['states']) and tile['entity'] not in dirty and dirty.isdisjoint(self.related_entities(tile))
if reuse and tile['entity'].startswith('weather.') and self.forecast_due(tile['entity']):
reuse = False
states.append(previous['states'][i] if reuse else await self.tile_message(i, tile))