Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options flow to Roborock #104345

Merged
merged 22 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
23 changes: 21 additions & 2 deletions homeassistant/components/roborock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady

from .const import CONF_BASE_URL, CONF_USER_DATA, DOMAIN, PLATFORMS
from .const import (
CONF_BASE_URL,
CONF_INCLUDE_SHARED,
CONF_USER_DATA,
DEFAULT_INCLUDE_SHARED,
DOMAIN,
PLATFORMS,
)
from .coordinator import RoborockDataUpdateCoordinator

SCAN_INTERVAL = timedelta(seconds=30)
Expand All @@ -28,6 +35,7 @@
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up roborock from a config entry."""
_LOGGER.debug("Integration async setup entry: %s", entry.as_dict())
entry.async_on_unload(entry.add_update_listener(update_listener))

user_data = UserData.from_dict(entry.data[CONF_USER_DATA])
api_client = RoborockApiClient(entry.data[CONF_USERNAME], entry.data[CONF_BASE_URL])
Expand All @@ -39,8 +47,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except RoborockException as err:
raise ConfigEntryNotReady("Failed getting Roborock home_data.") from err
_LOGGER.debug("Got home data %s", home_data)
all_devices: list[HomeDataDevice] = (
home_data.devices + home_data.received_devices
if entry.options.get(CONF_INCLUDE_SHARED, DEFAULT_INCLUDE_SHARED)
else home_data.devices
)
device_map: dict[str, HomeDataDevice] = {
device.duid: device for device in home_data.devices + home_data.received_devices
device.duid: device for device in all_devices
}
product_info: dict[str, HomeDataProduct] = {
product.id: product for product in home_data.products
Expand Down Expand Up @@ -152,3 +165,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
# Reload entry to update data
await hass.config_entries.async_reload(entry.entry_id)
119 changes: 115 additions & 4 deletions homeassistant/components/roborock/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,22 @@
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult

from .const import CONF_BASE_URL, CONF_ENTRY_CODE, CONF_USER_DATA, DOMAIN
from .const import (
CONF_BASE_URL,
CONF_ENTRY_CODE,
CONF_INCLUDE_SHARED,
CONF_USER_DATA,
DEFAULT_DRAWABLES,
DEFAULT_INCLUDE_SHARED,
DEFAULT_SIZES,
DOMAIN,
DRAWABLES,
MAPS,
SIZES,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -108,9 +121,6 @@ async def async_step_code(
CONF_USER_DATA: login_data.as_dict(),
},
)
await self.hass.config_entries.async_reload(
self.reauth_entry.entry_id
)
return self.async_abort(reason="reauth_successful")
return self._create_entry(self._client, self._username, login_data)

Expand Down Expand Up @@ -153,3 +163,104 @@ def _create_entry(
CONF_BASE_URL: client.base_url,
},
)

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return RoborockOptionsFlowHandler(config_entry)


class RoborockOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
"""Handle an option flow for Roborock."""

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
return self.async_show_menu(step_id="init", menu_options=[DOMAIN, MAPS])

async def async_step_roborock(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options domain wide."""
if user_input is not None:
return self.async_create_entry(
title="", data={**self.config_entry.options, **user_input}
)
return self.async_show_form(
step_id=DOMAIN,
data_schema=vol.Schema(
{
vol.Required(
CONF_INCLUDE_SHARED,
default=self.config_entry.options.get(
CONF_INCLUDE_SHARED, DEFAULT_INCLUDE_SHARED
),
): bool
}
),
)

async def async_step_maps(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Open the menu for the map options."""
return self.async_show_menu(step_id=MAPS, menu_options=[DRAWABLES, SIZES])

async def async_step_sizes(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the map object size options."""
if user_input is not None:
new_sizes = {
SIZES: {**self.config_entry.options.get(SIZES, {}), **user_input}
}
return self.async_create_entry(
title="", data={**self.config_entry.options, **new_sizes}
)
data_schema = {}
for size, default_value in DEFAULT_SIZES.items():
data_schema[
vol.Required(
size.value,
default=self.config_entry.options.get(SIZES, {}).get(
size, default_value
),
)
] = vol.All(vol.Coerce(float), vol.Range(min=0))
return self.async_show_form(
step_id=SIZES,
data_schema=vol.Schema(data_schema),
)

async def async_step_drawables(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the map object drawable options."""
if user_input is not None:
new_drawables = {
DRAWABLES: {
**self.config_entry.options.get(DRAWABLES, {}),
**user_input,
}
}
return self.async_create_entry(
title="", data={**self.config_entry.options, **new_drawables}
)
data_schema = {}
for drawable, default_value in DEFAULT_DRAWABLES.items():
data_schema[
vol.Required(
drawable.value,
default=self.config_entry.options.get(DRAWABLES, {}).get(
drawable, default_value
),
)
] = bool
return self.async_show_form(
step_id=DRAWABLES,
data_schema=vol.Schema(data_schema),
)
44 changes: 39 additions & 5 deletions homeassistant/components/roborock/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Constants for Roborock."""
from vacuum_map_parser_base.config.drawable import Drawable
from vacuum_map_parser_base.config.size import Size

from homeassistant.const import Platform

Expand All @@ -8,6 +9,44 @@
CONF_BASE_URL = "base_url"
CONF_USER_DATA = "user_data"

# domain options
CONF_INCLUDE_SHARED = "include_shared"
DEFAULT_INCLUDE_SHARED = True

# Option Flow steps
SIZES = "sizes"
DRAWABLES = "drawables"
MAPS = "maps"

DEFAULT_DRAWABLES = {
Drawable.CHARGER: True,
Drawable.CLEANED_AREA: False,
Drawable.GOTO_PATH: False,
Drawable.IGNORED_OBSTACLES: False,
Drawable.IGNORED_OBSTACLES_WITH_PHOTO: False,
Drawable.MOP_PATH: False,
Drawable.NO_CARPET_AREAS: False,
Drawable.NO_GO_AREAS: False,
Drawable.NO_MOPPING_AREAS: False,
Drawable.OBSTACLES: False,
Drawable.OBSTACLES_WITH_PHOTO: False,
Drawable.PATH: True,
Drawable.PREDICTED_PATH: False,
Drawable.VACUUM_POSITION: True,
Drawable.VIRTUAL_WALLS: False,
Drawable.ZONES: False,
}

DEFAULT_SIZES = {
Size.VACUUM_RADIUS: 6,
Size.PATH_WIDTH: 1,
Size.IGNORED_OBSTACLE_RADIUS: 3,
Size.IGNORED_OBSTACLE_WITH_PHOTO_RADIUS: 3,
Size.OBSTACLE_RADIUS: 3,
Size.OBSTACLE_WITH_PHOTO_RADIUS: 3,
Size.CHARGER_RADIUS: 6,
Size.MOP_PATH_WIDTH: 1,
}
PLATFORMS = [
Platform.BUTTON,
Platform.BINARY_SENSOR,
Expand All @@ -20,11 +59,6 @@
Platform.VACUUM,
]

IMAGE_DRAWABLES: list[Drawable] = [
Drawable.PATH,
Drawable.CHARGER,
Drawable.VACUUM_POSITION,
]

IMAGE_CACHE_INTERVAL = 90

Expand Down
30 changes: 26 additions & 4 deletions homeassistant/components/roborock/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from roborock import RoborockCommand
from vacuum_map_parser_base.config.color import ColorsPalette
from vacuum_map_parser_base.config.drawable import Drawable
from vacuum_map_parser_base.config.image_config import ImageConfig
from vacuum_map_parser_base.config.size import Sizes
from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser
Expand All @@ -18,7 +19,15 @@
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util

from .const import DOMAIN, IMAGE_CACHE_INTERVAL, IMAGE_DRAWABLES, MAP_SLEEP
from .const import (
DEFAULT_DRAWABLES,
DEFAULT_SIZES,
DOMAIN,
DRAWABLES,
IMAGE_CACHE_INTERVAL,
MAP_SLEEP,
SIZES,
)
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockCoordinatedEntity

Expand All @@ -33,10 +42,19 @@ async def async_setup_entry(
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
sizes = Sizes({**DEFAULT_SIZES, **config_entry.options.get(SIZES, {})})
drawables = [
drawable
for drawable, default_value in DEFAULT_DRAWABLES.items()
if config_entry.options.get(DRAWABLES, {}).get(drawable, default_value)
]
entities = list(
chain.from_iterable(
await asyncio.gather(
*(create_coordinator_maps(coord) for coord in coordinators.values())
*(
create_coordinator_maps(coord, sizes, drawables)
for coord in coordinators.values()
)
)
)
)
Expand All @@ -55,13 +73,15 @@ def __init__(
map_flag: int,
starting_map: bytes,
map_name: str,
sizes: Sizes,
drawables: list[Drawable],
) -> None:
"""Initialize a Roborock map."""
RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
ImageEntity.__init__(self, coordinator.hass)
self._attr_name = map_name
self.parser = RoborockMapDataParser(
ColorsPalette(), Sizes(), IMAGE_DRAWABLES, ImageConfig(), []
ColorsPalette(), sizes, drawables, ImageConfig(), []
)
self._attr_image_last_updated = dt_util.utcnow()
self.map_flag = map_flag
Expand Down Expand Up @@ -116,7 +136,7 @@ def _create_image(self, map_bytes: bytes) -> bytes:


async def create_coordinator_maps(
coord: RoborockDataUpdateCoordinator,
coord: RoborockDataUpdateCoordinator, sizes: Sizes, drawables: list[Drawable]
) -> list[RoborockMap]:
"""Get the starting map information for all maps for this device. The following steps must be done synchronously.

Expand Down Expand Up @@ -152,6 +172,8 @@ async def create_coordinator_maps(
roborock_map.mapFlag,
api_data,
roborock_map.name,
sizes,
drawables,
)
)
if len(maps.map_info) != 1:
Expand Down
59 changes: 59 additions & 0 deletions homeassistant/components/roborock/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,65 @@
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
"step": {
"init": {
"description": "Configure Roborock options.",
"menu_options": {
"roborock": "Roborock",
"maps": "Maps"
}
},
"roborock": {
"description": "Configure setting options for all Roborock devices.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this file to match the latest code changes.
A lot can be removed

"data": {
"include_shared": "Include shared devices"
}
},
"maps": {
"description": "Configure setting options for all of your Roborock map's images.",
"menu_options": {
"drawables": "Items to draw",
"sizes": "Sizes"
}
},
"sizes": {
"description": "Configure setting options for all of your Roborock maps' sizes.",
"data": {
"charger_radius": "Charger radius",
"ignored_obstacle_radius": "Ignored obstacle radius",
"ignored_obstacle_with_photo_radius": "Ignored obstacle with photo radius",
"mop_path_width": "Mop path width",
"obstacle_radius": "Obstacle radius",
"obstacle_with_photo_radius": "Obstacle with photo radius",
"vacuum_radius": "Vacuum radius",
"path_width": "Path width"
}
},
"drawables": {
"description": "Specify which features to draw on the map.",
"data": {
"charger": "Charger",
"cleaned_area": "Cleaned area",
"goto_path": "Go-to path",
"ignored_obstacles": "Ignored obstacles",
"ignored_obstacles_with_photo": "Ignored obstacles with photo",
"mop_path": "Mop path",
"no_carpet_zones": "No carpet zones",
"no_go_zones": "No-go zones",
"no_mopping_zones": "No mopping zones",
"obstacles": "Obstacles",
"obstacles_with_photo": "Obstacles with photo",
"path": "Path",
"predicted_path": "Predicted path",
"room_names": "Room names",
"vacuum_position": "Vacuum position",
"virtual_walls": "Virtual walls",
"zones": "Zones"
}
}
}
},
"entity": {
"binary_sensor": {
"in_cleaning": {
Expand Down
Loading