From db39b317e91cf17e77b5b7d46f8672375e6b361e Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 22 Mar 2025 11:15:33 -0400 Subject: [PATCH] init commit --- .gitignore | 1 + README.md | 50 +++++++++ .../roborock_custom_map/__init__.py | 37 +++++++ .../roborock_custom_map/config_flow.py | 24 ++++ .../roborock_custom_map/const.py | 3 + .../roborock_custom_map/image.py | 104 ++++++++++++++++++ .../roborock_custom_map/manifest.json | 13 +++ .../roborock_custom_map/translations/en.json | 7 ++ hacs.json | 6 + 9 files changed, 245 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 custom_components/roborock_custom_map/__init__.py create mode 100644 custom_components/roborock_custom_map/config_flow.py create mode 100644 custom_components/roborock_custom_map/const.py create mode 100644 custom_components/roborock_custom_map/image.py create mode 100644 custom_components/roborock_custom_map/manifest.json create mode 100644 custom_components/roborock_custom_map/translations/en.json create mode 100644 hacs.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..890b020 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Roborock Custom Map + +This allows you to use the core Roborock integration with the [Xiaomi Map Card](https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card) + +If you would like to support me, you can do so here: + +[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] + +[![PaypalMe][paypalmebadge]][paypalme] + +### Setup + +1. Install the [Roborock Core Integration](https://my.home-assistant.io/redirect/config_flow_start?domain=roborock) and set it up +2. It is recommended that you first disable the Image entities within the core integration. Open each image entity, hit the gear icon, then trigger the toggle by enabled. +3. Install this integration +4. This integration works by piggybacking off of the Core integration, so the Core integration will do all the data updating to help prevent rate-limits. But that means that the core integration must be setup and loaded first. If you run into any issues, make sure the Roborock integration is loaded first, and then reload this one. +5. Setup the map card like normal! An example configuration would look like +```yaml +type: custom:xiaomi-vacuum-map-card +vacuum_platform: roborock +entity: vacuum.s7 +map_source: + camera: image.s7_downstairs_full_custom +calibration_source: + camera: true +``` +6. You can hit 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' + + +### Installation + +### Installing via HACS +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Lash-L&repository=RoborockCustomMap&category=integration) + +or + +1. Go to HACS->Integrations +1. Add this repo(https://github.com/Lash-L/RoborockCustomMap) into your HACS custom repositories +1. Search for Roborock Custom Map and Download it +1. Restart your HomeAssistant +1. Go to Settings->Devices & Services +1. Add the Roborock Custom Map integration + + + +[buymecoffee]: https://www.buymeacoffee.com/LashL +[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge +[paypalme]: https://paypal.me/LLashley304 +[paypalmebadge]: https://cdn.rawgit.com/twolfson/paypal-github-button/1.0.0/dist/button.svg +[hacsbutton]: https://my.home-assistant.io/redirect/hacs_repository/?owner=Lash-L&repository=tempofit&category=integration diff --git a/custom_components/roborock_custom_map/__init__.py b/custom_components/roborock_custom_map/__init__.py new file mode 100644 index 0000000..bc1742c --- /dev/null +++ b/custom_components/roborock_custom_map/__init__.py @@ -0,0 +1,37 @@ +"""Roborock Custom Map integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntryState +from homeassistant.exceptions import ConfigEntryNotReady + +PLATFORMS = [Platform.IMAGE] + + +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") + coordinators = [] + + async def unload_this_entry(): + await hass.config_entries.async_reload(entry.entry_id) + + for r_entry in roborock_entries: + if r_entry.state == ConfigEntryState.LOADED: + coordinators.extend(r_entry.runtime_data.v1) + # If any unload, then we should reload as well in case there are major changes. + r_entry.async_on_unload(unload_this_entry) + if len(coordinators) == 0: + raise ConfigEntryNotReady("No Roborock entries loaded. Cannot start.") + entry.runtime_data = coordinators + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/roborock_custom_map/config_flow.py b/custom_components/roborock_custom_map/config_flow.py new file mode 100644 index 0000000..51791ef --- /dev/null +++ b/custom_components/roborock_custom_map/config_flow.py @@ -0,0 +1,24 @@ +"""Config flow for Roborock Custom Map integration.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Roborock Custom Map.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Roborock Custom Map", data={}) diff --git a/custom_components/roborock_custom_map/const.py b/custom_components/roborock_custom_map/const.py new file mode 100644 index 0000000..8cdbb6f --- /dev/null +++ b/custom_components/roborock_custom_map/const.py @@ -0,0 +1,3 @@ +"""Constants for Roborock Custom Map integration.""" + +DOMAIN = "roborock_custom_map" diff --git a/custom_components/roborock_custom_map/image.py b/custom_components/roborock_custom_map/image.py new file mode 100644 index 0000000..eed9c39 --- /dev/null +++ b/custom_components/roborock_custom_map/image.py @@ -0,0 +1,104 @@ +"""Support for Roborock image.""" + +from datetime import datetime +import logging + +from homeassistant.components.image import ImageEntity +from homeassistant.components.roborock.coordinator import RoborockDataUpdateCoordinator +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.helpers.entity_platform import AddConfigEntryEntitiesCallback + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Roborock image platform.""" + + async_add_entities( + ( + RoborockMap( + config_entry, + f"{coord.duid_slug}_custom_map_{map_info.name}", + coord, + map_info.flag, + map_info.name, + ) + for coord in config_entry.runtime_data + for map_info in coord.maps.values() + ), + ) + + +class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity): + """A class to let you visualize the map.""" + + _attr_has_entity_name = True + image_last_updated: datetime + _attr_name: str + + def __init__( + self, + config_entry: ConfigEntry, + unique_id: str, + coordinator: RoborockDataUpdateCoordinator, + map_flag: int, + map_name: str, + ) -> None: + """Initialize a Roborock map.""" + RoborockCoordinatedEntityV1.__init__(self, unique_id, coordinator) + ImageEntity.__init__(self, coordinator.hass) + self.config_entry = config_entry + self._attr_name = map_name + "_custom" + self.map_flag = map_flag + self.cached_map = b"" + self._attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def is_selected(self) -> bool: + """Return if this map is the currently selected map.""" + return self.map_flag == self.coordinator.current_map + + async def async_added_to_hass(self) -> None: + """When entity is added to hass load any previously cached maps from disk.""" + await super().async_added_to_hass() + self._attr_image_last_updated = self.coordinator.maps[ + self.map_flag + ].last_updated + self.async_write_ha_state() + + def _handle_coordinator_update(self) -> None: + # If the coordinator has updated the map, we can update the image. + self._attr_image_last_updated = self.coordinator.maps[ + self.map_flag + ].last_updated + + super()._handle_coordinator_update() + + async def async_image(self) -> bytes | None: + """Get the cached image.""" + return self.coordinator.maps[self.map_flag].image + + @property + def extra_state_attributes(self): + map_data = self.coordinator.maps[self.map_flag].map_data + if map_data is None: + return {} + for room in map_data.rooms.values(): + room.name = self.coordinator.maps[self.map_flag].rooms.get(room.number) + + return { + "calibration_points": self.coordinator.maps[ + self.map_flag + ].map_data.calibration(), + "rooms": map_data.rooms, + "zones": map_data.zones, + } diff --git a/custom_components/roborock_custom_map/manifest.json b/custom_components/roborock_custom_map/manifest.json new file mode 100644 index 0000000..83570b8 --- /dev/null +++ b/custom_components/roborock_custom_map/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "roborock_custom_map", + "name": "Roborock Custom Map", + "codeowners": ["@Lash-L"], + "config_flow": true, + "dependencies": ["roborock"], + "documentation": "https://github.com/Lash-L/RoborockCustomMap", + "iot_class": "local_polling", + "issue_tracker": "https://github.com/Lash-L/RoborockCustomMap/issues", + "requirements": [], + + "version": "0.1.0" +} diff --git a/custom_components/roborock_custom_map/translations/en.json b/custom_components/roborock_custom_map/translations/en.json new file mode 100644 index 0000000..9ef6414 --- /dev/null +++ b/custom_components/roborock_custom_map/translations/en.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..3179090 --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "Roborock Custom Map", + "hacs": "0.1.0", + "homeassistant": "2025.4", + "domains": ["image"] +} \ No newline at end of file