Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.idea
.idea
__pycache__/
34 changes: 16 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Roborock Custom Map

you MUST be on 2025.4b or later
you MUST be on 2025.12.1 or later

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

Expand All @@ -26,27 +26,25 @@ map_source:
calibration_source:
camera: true
```
### Map rotation (new)

If your map is displayed sideways or upside down, you can rotate the map directly in Home Assistant.
### Custom Floor Plan

This integration provides a **Select entity per map** to control rotation:
- `select.<...>_rotation`
- Options: `0°`, `90°`, `180°`, `270°` (labels depend on your HA language)
You can now override a Stock Floor Plan with your own Custom Floor Plan
(for example a tidied-up or stylized version).

This rotates **both**:
- the map image
- and the calibration points used by the Xiaomi Vacuum Map Card
(so rooms/zones and interactions stay aligned after rotation)
#### How?
1. `Settings` → `Devices & Services` → `Roborock Custom Map` → press `Configure`.
2. Pick a Floor (only if > 1 available).
3. Choose your Floor Plan (PNG, JPEG or WebP); a Live Preview appears as you do so.
4. Press `Submit` — the Floor Plan applies immediately as-is, with no adjustments.
5. On the next view, optionally adjust the horizontal/vertical offset/scale
of the Custom Floor Plan, or the relative rotation of the Physical Walls.
The Physical Walls are imposed over the Custom Floor Plan for your convenience.
The Live Preview refreshes in near-real-time.
6. Press `Submit` once more — to keep the adjustments,
or simply press `X` — to keep the Floor Plan as-is.

**How to use**
1. Go to **Settings → Devices & services → Roborock Custom Map**
2. Open the device/entities list
3. Find the `… rotation` select entity for your map and choose the correct rotation

No reload is required; the map updates immediately.

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'
Reopen `Configure` **at any time** to Adjust, Replace, or Remove the Custom Floor Plan.

### Installation

Expand Down
64 changes: 59 additions & 5 deletions custom_components/roborock_custom_map/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,34 @@

from __future__ import annotations

import shutil

from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send

from .const import CONF_MAP_ROTATION, DOMAIN
from .const import (
CONF_BG_OVERRIDES,
CONF_MAP_ROTATION,
DATA_LAST_BG_OVERRIDES,
DATA_PREVIEW_SESSIONS,
DOMAIN,
signal_map_refresh,
)
from .map_render import clear_scaled_backgrounds, override_dir

PLATFORMS = [Platform.IMAGE, Platform.SELECT]


def _overrides_snapshot(entry: ConfigEntry) -> dict[str, dict]:
return {
key: dict(record)
for key, record in entry.options.get(CONF_BG_OVERRIDES, {}).items()
}


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Roborock Custom map from a config entry."""
roborock_entries = hass.config_entries.async_entries("roborock")
Expand All @@ -31,18 +49,54 @@ def unload_this_entry() -> None:

entry.runtime_data = coordinators

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(entry.entry_id, {})
hass.data[DOMAIN][entry.entry_id].setdefault(CONF_MAP_ROTATION, {})
data = hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
data.setdefault(CONF_MAP_ROTATION, {})
data.setdefault(DATA_PREVIEW_SESSIONS, {})
data[DATA_LAST_BG_OVERRIDES] = _overrides_snapshot(entry)

entry.async_on_unload(entry.add_update_listener(_async_update_listener))

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Refresh only the maps whose background override actually changed.

Image entities read entry.options at render time, so a dispatcher nudge is
enough — no entry reload, which would tear down entities and any other
open tuning session.
"""
data = hass.data.get(DOMAIN, {}).get(entry.entry_id)
new = _overrides_snapshot(entry)
old = data.get(DATA_LAST_BG_OVERRIDES) if data is not None else None
if data is not None:
data[DATA_LAST_BG_OVERRIDES] = new

if old is None:
changed = set(new)
else:
changed = {
key for key in old.keys() | new.keys() if old.get(key) != new.get(key)
}
for key in changed:
async_dispatcher_send(hass, signal_map_refresh(entry.entry_id, key))


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unloaded:
hass.data.get(DOMAIN, {}).pop(entry.entry_id, None)
return unloaded
clear_scaled_backgrounds()
return unloaded


async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Clean up stored background override files when the last entry is removed."""
if hass.config_entries.async_entries(DOMAIN):
return
await hass.async_add_executor_job(
lambda: shutil.rmtree(override_dir(hass), ignore_errors=True)
)
Loading