mirror of
https://github.com/Lash-L/RoborockCustomMap.git
synced 2026-07-26 16:18:32 +02:00
Fix rotation conflicts between multiple Roborock devices (#40)
* Add map rotation constants Introduce rotation configuration constants for map image handling. Adds rotation options (0, 90, 180, 270) and dispatcher signal name. * Add per-map rotation select entity Add SelectEntity to control map rotation per map_flag. Rotation value is persisted via RestoreEntity and stored in hass.data. Dispatcher signal notifies image entities when rotation changes. * Enable rotation select platform and initialize storage Register SELECT platform and initialize rotation storage in hass.data. Add proper unload cleanup and reload behavior. * Add backend map rotation with executor offloading Implement backend image rotation using Pillow. Rotation is applied in async_add_executor_job to avoid blocking the event loop. Includes defensive validation and fallback handling. * Add translations for rotation select entity Add English and German translations for map rotation select entity. Includes user-friendly labels for rotation options. * Document map rotation select entity in README Add documentation for the per-map rotation select entity. Explains: - How to rotate maps (0/90/180/270) - Where to find the rotation select entity - That calibration points are rotated as well - That no reload is required Also clarifies usage with Xiaomi Vacuum Map Card. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Store rotation state per device and map instead of only map_flag. This prevents multiple Roborock devices with identical map_flag values from sharing rotation state and dispatcher signals. * Fix image rotation lookup for multiple Roborock devices * Fix thread-safe state update on rotation change I found and fixed a thread-safety issue in the rotation update signal. The image entity now schedules the state update back onto the Home Assistant event loop before calling `async_write_ha_state()`, instead of calling it directly from the dispatcher callback. --------- Co-authored-by: Luke Lashley <conway220@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Luke Lashley
Copilot
parent
52b4ffec4a
commit
2ee83993b7
@@ -15,7 +15,7 @@ from homeassistant.components.roborock.coordinator import RoborockDataUpdateCoor
|
||||
from homeassistant.components.roborock.entity import RoborockCoordinatedEntityV1
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -111,6 +111,7 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
|
||||
self.config_entry = config_entry
|
||||
self.map_flag = map_flag
|
||||
self.rotation_key = f"{coordinator.duid_slug}_{map_flag}"
|
||||
self._home_trait = home_trait
|
||||
|
||||
if not map_name:
|
||||
@@ -145,7 +146,7 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.map_flag}",
|
||||
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.rotation_key}",
|
||||
self._handle_rotation_changed,
|
||||
)
|
||||
)
|
||||
@@ -153,6 +154,12 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _handle_rotation_changed(self) -> None:
|
||||
"""Rotation changed; schedule state update in the event loop."""
|
||||
self.hass.loop.call_soon_threadsafe(self._async_handle_rotation_changed)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_handle_rotation_changed(self) -> None:
|
||||
"""Rotation changed; bump last_updated to bust the image cache."""
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self.async_write_ha_state()
|
||||
@@ -184,7 +191,7 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
self.hass.data.get(DOMAIN, {})
|
||||
.get(self.config_entry.entry_id, {})
|
||||
.get(CONF_MAP_ROTATION, {})
|
||||
.get(self.map_flag, DEFAULT_MAP_ROTATION)
|
||||
.get(self.rotation_key, DEFAULT_MAP_ROTATION)
|
||||
)
|
||||
|
||||
if rotation not in MAP_ROTATION_OPTIONS:
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.roborock.coordinator import RoborockDataUpdateCoordinator
|
||||
from homeassistant.components.roborock.entity import RoborockCoordinatedEntityV1
|
||||
from homeassistant.components.select import SelectEntity
|
||||
@@ -22,8 +20,6 @@ from .const import (
|
||||
SIGNAL_ROTATION_CHANGED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@@ -52,6 +48,7 @@ class RoborockMapRotationSelect(RoborockCoordinatedEntityV1, RestoreEntity, Sele
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_translation_key = "rotation"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -66,6 +63,7 @@ class RoborockMapRotationSelect(RoborockCoordinatedEntityV1, RestoreEntity, Sele
|
||||
|
||||
self.config_entry = config_entry
|
||||
self.map_flag = map_flag
|
||||
self.rotation_key = f"{coordinator.duid_slug}_{map_flag}"
|
||||
|
||||
if not map_name:
|
||||
map_name = f"Map {map_flag}"
|
||||
@@ -73,7 +71,6 @@ class RoborockMapRotationSelect(RoborockCoordinatedEntityV1, RestoreEntity, Sele
|
||||
self._attr_name = f"{map_name} rotation"
|
||||
self._attr_options = [str(v) for v in MAP_ROTATION_OPTIONS]
|
||||
self._attr_current_option = str(DEFAULT_MAP_ROTATION)
|
||||
self._attr_translation_key = "rotation"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore previous rotation setting and store in hass.data."""
|
||||
@@ -85,7 +82,7 @@ class RoborockMapRotationSelect(RoborockCoordinatedEntityV1, RestoreEntity, Sele
|
||||
|
||||
# Persist selection for the image entity to read
|
||||
self.hass.data[DOMAIN][self.config_entry.entry_id][CONF_MAP_ROTATION][
|
||||
self.map_flag
|
||||
self.rotation_key
|
||||
] = int(self._attr_current_option)
|
||||
|
||||
self.async_write_ha_state()
|
||||
@@ -98,13 +95,13 @@ class RoborockMapRotationSelect(RoborockCoordinatedEntityV1, RestoreEntity, Sele
|
||||
self._attr_current_option = option
|
||||
|
||||
self.hass.data[DOMAIN][self.config_entry.entry_id][CONF_MAP_ROTATION][
|
||||
self.map_flag
|
||||
self.rotation_key
|
||||
] = int(option)
|
||||
|
||||
# Notify the image entity to bust the cache via image_last_updated bump
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.map_flag}",
|
||||
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.rotation_key}",
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
Reference in New Issue
Block a user