Skip to content

Commit 2ee8399

Browse files
TheHangMan97Lash-LCopilot
authored
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>
1 parent 52b4ffe commit 2ee8399

2 files changed

Lines changed: 15 additions & 11 deletions

File tree

custom_components/roborock_custom_map/image.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from homeassistant.components.roborock.entity import RoborockCoordinatedEntityV1
1616
from homeassistant.config_entries import ConfigEntry
1717
from homeassistant.const import EntityCategory
18-
from homeassistant.core import HomeAssistant
18+
from homeassistant.core import HomeAssistant, callback
1919
from homeassistant.exceptions import HomeAssistantError
2020
from homeassistant.helpers.dispatcher import async_dispatcher_connect
2121
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -111,6 +111,7 @@ def __init__(
111111

112112
self.config_entry = config_entry
113113
self.map_flag = map_flag
114+
self.rotation_key = f"{coordinator.duid_slug}_{map_flag}"
114115
self._home_trait = home_trait
115116

116117
if not map_name:
@@ -145,14 +146,20 @@ async def async_added_to_hass(self) -> None:
145146
self.async_on_remove(
146147
async_dispatcher_connect(
147148
self.hass,
148-
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.map_flag}",
149+
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.rotation_key}",
149150
self._handle_rotation_changed,
150151
)
151152
)
152153

153154
self.async_write_ha_state()
154155

155156
def _handle_rotation_changed(self) -> None:
157+
"""Rotation changed; schedule state update in the event loop."""
158+
self.hass.loop.call_soon_threadsafe(self._async_handle_rotation_changed)
159+
160+
161+
@callback
162+
def _async_handle_rotation_changed(self) -> None:
156163
"""Rotation changed; bump last_updated to bust the image cache."""
157164
self._attr_image_last_updated = dt_util.utcnow()
158165
self.async_write_ha_state()
@@ -184,7 +191,7 @@ def _get_rotation(self) -> int:
184191
self.hass.data.get(DOMAIN, {})
185192
.get(self.config_entry.entry_id, {})
186193
.get(CONF_MAP_ROTATION, {})
187-
.get(self.map_flag, DEFAULT_MAP_ROTATION)
194+
.get(self.rotation_key, DEFAULT_MAP_ROTATION)
188195
)
189196

190197
if rotation not in MAP_ROTATION_OPTIONS:

custom_components/roborock_custom_map/select.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from __future__ import annotations
44

5-
import logging
6-
75
from homeassistant.components.roborock.coordinator import RoborockDataUpdateCoordinator
86
from homeassistant.components.roborock.entity import RoborockCoordinatedEntityV1
97
from homeassistant.components.select import SelectEntity
@@ -22,8 +20,6 @@
2220
SIGNAL_ROTATION_CHANGED,
2321
)
2422

25-
_LOGGER = logging.getLogger(__name__)
26-
2723
PARALLEL_UPDATES = 0
2824

2925

@@ -52,6 +48,7 @@ class RoborockMapRotationSelect(RoborockCoordinatedEntityV1, RestoreEntity, Sele
5248

5349
_attr_has_entity_name = True
5450
_attr_entity_category = EntityCategory.CONFIG
51+
_attr_translation_key = "rotation"
5552

5653
def __init__(
5754
self,
@@ -66,14 +63,14 @@ def __init__(
6663

6764
self.config_entry = config_entry
6865
self.map_flag = map_flag
66+
self.rotation_key = f"{coordinator.duid_slug}_{map_flag}"
6967

7068
if not map_name:
7169
map_name = f"Map {map_flag}"
7270

7371
self._attr_name = f"{map_name} rotation"
7472
self._attr_options = [str(v) for v in MAP_ROTATION_OPTIONS]
7573
self._attr_current_option = str(DEFAULT_MAP_ROTATION)
76-
self._attr_translation_key = "rotation"
7774

7875
async def async_added_to_hass(self) -> None:
7976
"""Restore previous rotation setting and store in hass.data."""
@@ -85,7 +82,7 @@ async def async_added_to_hass(self) -> None:
8582

8683
# Persist selection for the image entity to read
8784
self.hass.data[DOMAIN][self.config_entry.entry_id][CONF_MAP_ROTATION][
88-
self.map_flag
85+
self.rotation_key
8986
] = int(self._attr_current_option)
9087

9188
self.async_write_ha_state()
@@ -98,13 +95,13 @@ async def async_select_option(self, option: str) -> None:
9895
self._attr_current_option = option
9996

10097
self.hass.data[DOMAIN][self.config_entry.entry_id][CONF_MAP_ROTATION][
101-
self.map_flag
98+
self.rotation_key
10299
] = int(option)
103100

104101
# Notify the image entity to bust the cache via image_last_updated bump
105102
async_dispatcher_send(
106103
self.hass,
107-
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.map_flag}",
104+
f"{SIGNAL_ROTATION_CHANGED}_{self.config_entry.entry_id}_{self.rotation_key}",
108105
)
109106

110107
self.async_write_ha_state()

0 commit comments

Comments
 (0)