Skip to content

Commit 6c126a7

Browse files
committed
Add possibility to per-map override a Custom Floor Plan
1 parent 8cf623e commit 6c126a7

12 files changed

Lines changed: 1872 additions & 112 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
.idea
1+
.idea
2+
__pycache__/

README.md

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Roborock Custom Map
22

3-
you MUST be on 2025.4b or later
3+
you MUST be on 2025.12.1 or later
44

55
This allows you to use the core Roborock integration with the [Xiaomi Map Card](https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card)
66

@@ -26,27 +26,25 @@ map_source:
2626
calibration_source:
2727
camera: true
2828
```
29-
### Map rotation (new)
3029
31-
If your map is displayed sideways or upside down, you can rotate the map directly in Home Assistant.
30+
### Custom Floor Plan
3231
33-
This integration provides a **Select entity per map** to control rotation:
34-
- `select.<...>_rotation`
35-
- Options: ``, `90°`, `180°`, `270°` (labels depend on your HA language)
32+
You can now override a Stock Floor Plan with your own Custom Floor Plan
33+
(for example a tidied-up or stylized version).
3634
37-
This rotates **both**:
38-
- the map image
39-
- and the calibration points used by the Xiaomi Vacuum Map Card
40-
(so rooms/zones and interactions stay aligned after rotation)
35+
#### How?
36+
1. `Settings` → `Devices & Services` → `Roborock Custom Map` → press `Configure`.
37+
2. Pick a Floor (only if > 1 available).
38+
3. Choose your Floor Plan (PNG, JPEG or WebP); a Live Preview appears as you do so.
39+
4. Press `Submit` — the Floor Plan applies immediately as-is, with no adjustments.
40+
5. On the next view, optionally adjust the horizontal/vertical offset/scale
41+
of the Custom Floor Plan, or the relative rotation of the Physical Walls.
42+
The Physical Walls are imposed over the Custom Floor Plan for your convenience.
43+
The Live Preview refreshes in near-real-time.
44+
6. Press `Submit` once more — to keep the adjustments,
45+
or simply press `X` — to keep the Floor Plan as-is.
4146

42-
**How to use**
43-
1. Go to **Settings → Devices & services → Roborock Custom Map**
44-
2. Open the device/entities list
45-
3. Find the `… rotation` select entity for your map and choose the correct rotation
46-
47-
No reload is required; the map updates immediately.
48-
49-
6. You can hit Edit on the card and then Generate Room Configs to allow for cleaning of rooms. It might generate extra keys, so check the yaml and make sure there are no extra 'predefined_sections'
47+
Reopen `Configure` **at any time** to Adjust, Replace, or Remove the Custom Floor Plan.
5048

5149
### Installation
5250

custom_components/roborock_custom_map/__init__.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,34 @@
22

33
from __future__ import annotations
44

5+
import shutil
6+
57
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
68
from homeassistant.const import Platform
79
from homeassistant.core import HomeAssistant, callback
810
from homeassistant.exceptions import ConfigEntryNotReady
11+
from homeassistant.helpers.dispatcher import async_dispatcher_send
912

10-
from .const import CONF_MAP_ROTATION, DOMAIN
13+
from .const import (
14+
CONF_BG_OVERRIDES,
15+
CONF_MAP_ROTATION,
16+
DATA_LAST_BG_OVERRIDES,
17+
DATA_PREVIEW_SESSIONS,
18+
DOMAIN,
19+
signal_map_refresh,
20+
)
21+
from .map_render import clear_scaled_backgrounds, override_dir
1122

1223
PLATFORMS = [Platform.IMAGE, Platform.SELECT]
1324

1425

26+
def _overrides_snapshot(entry: ConfigEntry) -> dict[str, dict]:
27+
return {
28+
key: dict(record)
29+
for key, record in entry.options.get(CONF_BG_OVERRIDES, {}).items()
30+
}
31+
32+
1533
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
1634
"""Set up Roborock Custom map from a config entry."""
1735
roborock_entries = hass.config_entries.async_entries("roborock")
@@ -31,18 +49,54 @@ def unload_this_entry() -> None:
3149

3250
entry.runtime_data = coordinators
3351

34-
hass.data.setdefault(DOMAIN, {})
35-
hass.data[DOMAIN].setdefault(entry.entry_id, {})
36-
hass.data[DOMAIN][entry.entry_id].setdefault(CONF_MAP_ROTATION, {})
52+
data = hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
53+
data.setdefault(CONF_MAP_ROTATION, {})
54+
data.setdefault(DATA_PREVIEW_SESSIONS, {})
55+
data[DATA_LAST_BG_OVERRIDES] = _overrides_snapshot(entry)
56+
57+
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
3758

3859
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
3960

4061
return True
4162

4263

64+
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
65+
"""Refresh only the maps whose background override actually changed.
66+
67+
Image entities read entry.options at render time, so a dispatcher nudge is
68+
enough — no entry reload, which would tear down entities and any other
69+
open tuning session.
70+
"""
71+
data = hass.data.get(DOMAIN, {}).get(entry.entry_id)
72+
new = _overrides_snapshot(entry)
73+
old = data.get(DATA_LAST_BG_OVERRIDES) if data is not None else None
74+
if data is not None:
75+
data[DATA_LAST_BG_OVERRIDES] = new
76+
77+
if old is None:
78+
changed = set(new)
79+
else:
80+
changed = {
81+
key for key in old.keys() | new.keys() if old.get(key) != new.get(key)
82+
}
83+
for key in changed:
84+
async_dispatcher_send(hass, signal_map_refresh(entry.entry_id, key))
85+
86+
4387
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
4488
"""Unload a config entry."""
4589
unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
4690
if unloaded:
4791
hass.data.get(DOMAIN, {}).pop(entry.entry_id, None)
48-
return unloaded
92+
clear_scaled_backgrounds()
93+
return unloaded
94+
95+
96+
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
97+
"""Clean up stored background override files when the last entry is removed."""
98+
if hass.config_entries.async_entries(DOMAIN):
99+
return
100+
await hass.async_add_executor_job(
101+
lambda: shutil.rmtree(override_dir(hass), ignore_errors=True)
102+
)

0 commit comments

Comments
 (0)