"""Config flow for the Yandex Smart Home integration.""" from __future__ import annotations from enum import StrEnum import logging from typing import TYPE_CHECKING, cast from aiohttp import ClientConnectorError, ClientResponseError from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import CONF_ENTITIES, CONF_ID, CONF_NAME, CONF_PLATFORM, CONF_TOKEN from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowHandler from homeassistant.helpers import entity_registry as er, network, selector from homeassistant.helpers.entityfilter import CONF_INCLUDE_ENTITIES, FILTER_SCHEMA, EntityFilter from homeassistant.helpers.selector import ( BooleanSelector, LabelSelector, LabelSelectorConfig, SelectOptionDict, SelectSelector, SelectSelectorConfig, SelectSelectorMode, TextSelector, ) from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import voluptuous as vol from . import DOMAIN, cloud from .const import ( CLOUD_BASE_URL, CONF_CLOUD_INSTANCE, CONF_CLOUD_INSTANCE_CONNECTION_TOKEN, CONF_CLOUD_INSTANCE_ID, CONF_CLOUD_INSTANCE_OTP, CONF_CLOUD_INSTANCE_PASSWORD, CONF_CONNECTION_TYPE, CONF_ENTRY_ALIASES, CONF_FILTER, CONF_FILTER_SOURCE, CONF_LABEL, CONF_LINKED_PLATFORMS, CONF_SKILL, CONF_USER_ID, ConnectionType, EntityFilterSource, ) from .helpers import SmartHomePlatform if TYPE_CHECKING: from homeassistant.config_entries import ConfigFlowContext # noqa: F401 from . import YandexSmartHome _LOGGER = logging.getLogger(__name__) DEFAULT_CONFIG_ENTRY_TITLE = "Yandex Smart Home" PRE_V1_DIRECT_CONFIG_ENTRY_TITLE = "YSH: Direct" # TODO: remove after v1.1 release USER_NONE = "none" class MaintenanceAction(StrEnum): REVOKE_OAUTH_TOKENS = "revoke_oauth_tokens" UNLINK_ALL_PLATFORMS = "unlink_all_platforms" RESET_CLOUD_INSTANCE_CONNECTION_TOKEN = "reset_cloud_instance_connection_token" TRANSFER_ENTITY_FILTER_FROM_YAML = "transfer_entity_filter_from_yaml" CONNECTION_TYPE_SELECTOR = SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.LIST, translation_key=CONF_CONNECTION_TYPE, options=[ SelectOptionDict(value=ConnectionType.CLOUD, label=ConnectionType.CLOUD), SelectOptionDict(value=ConnectionType.CLOUD_PLUS, label=ConnectionType.CLOUD_PLUS), SelectOptionDict(value=ConnectionType.DIRECT, label=ConnectionType.DIRECT), ], ), ) PLATFORM_SELECTOR = SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.LIST, translation_key=CONF_PLATFORM, options=[ SelectOptionDict(value=SmartHomePlatform.YANDEX, label=SmartHomePlatform.YANDEX), SelectOptionDict(value=SmartHomePlatform.VK, label=SmartHomePlatform.VK), ], ), ) FILTER_SOURCE_SELECTOR = SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.LIST, translation_key=CONF_FILTER_SOURCE, options=[ SelectOptionDict(value=EntityFilterSource.CONFIG_ENTRY, label=EntityFilterSource.CONFIG_ENTRY), SelectOptionDict( value=EntityFilterSource.GET_FROM_CONFIG_ENTRY, label=EntityFilterSource.GET_FROM_CONFIG_ENTRY ), SelectOptionDict(value=EntityFilterSource.LABEL, label=EntityFilterSource.LABEL), SelectOptionDict(value=EntityFilterSource.YAML, label=EntityFilterSource.YAML), ], ), ) class BaseFlowHandler(FlowHandler["ConfigFlowContext", ConfigFlowResult]): """Handle shared steps between config and options flow for Yandex Smart Home.""" def __init__(self) -> None: """Initialize a flow handler.""" self._options: ConfigType = {} self._data: ConfigType = {} self._entry: ConfigEntry | None = None super().__init__() async def _async_step_skill_direct( self, platform: SmartHomePlatform, user_input: ConfigType | None = None ) -> ConfigFlowResult: """Choose skill settings for direct connection.""" errors = {} description_placeholders = {"external_url": self._get_external_url()} entry_skill = self._options.get(CONF_SKILL, {}) if DOMAIN not in self.hass.data: await async_setup_component(self.hass, DOMAIN, {}) # expose http endpoints for skill validation if user_input is not None: if existed_entry := self._get_direct_connection_entry( platform=platform, user_id=user_input[CONF_USER_ID], ): description_placeholders["entry_title"] = existed_entry.title errors["base"] = "already_configured" else: self._options[CONF_SKILL] = user_input if self._entry: if user_input[CONF_ID] != entry_skill.get(CONF_ID) or user_input[CONF_USER_ID] != entry_skill.get( CONF_USER_ID ): self._data[CONF_LINKED_PLATFORMS] = [] self.hass.config_entries.async_update_entry( self._entry, title=await async_config_entry_title(self.hass, self._data, self._options), data=self._data, ) return await self.async_step_done() return await self.async_step_expose_settings() data_schema = vol.Schema( { vol.Required(CONF_USER_ID, default=entry_skill.get(CONF_USER_ID)): await _async_get_user_selector( self.hass, mode=SelectSelectorMode.DROPDOWN, required=True ), vol.Required(CONF_ID, default=entry_skill.get(CONF_ID)): TextSelector(), vol.Required(CONF_TOKEN, default=entry_skill.get(CONF_TOKEN)): TextSelector(), }, ) if platform == SmartHomePlatform.VK: data_schema = vol.Schema( { vol.Required(CONF_USER_ID, default=entry_skill.get(CONF_USER_ID)): await _async_get_user_selector( self.hass, mode=SelectSelectorMode.DROPDOWN, required=True ), vol.Required(CONF_ID, default=entry_skill.get(CONF_ID)): TextSelector(), }, ) return self.async_show_form( step_id=f"skill_{platform}_direct", data_schema=data_schema, errors=errors, description_placeholders=description_placeholders, ) async def async_step_skill_yandex_direct(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose skill settings for direct connection to the Yandex Smart Home platform.""" return await self._async_step_skill_direct(SmartHomePlatform.YANDEX, user_input) async def async_step_skill_vk_direct(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose skill settings for direct connection to the VK Smart Home platform.""" return await self._async_step_skill_direct(SmartHomePlatform.VK, user_input) async def _async_step_skill_cloud_plus( self, platform: SmartHomePlatform, user_input: ConfigType | None = None ) -> ConfigFlowResult: """Choose skill settings for cloud plus connection.""" errors: dict[str, str] = {} description_placeholders = { "cloud_base_url": CLOUD_BASE_URL, "instance_id": self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_ID], } entry_skill = self._options.get(CONF_SKILL, {}) if user_input is not None: self._options[CONF_SKILL] = user_input if self._entry: if user_input[CONF_ID] != entry_skill.get(CONF_ID): self._data[CONF_LINKED_PLATFORMS] = [] if user_input[CONF_ID] != entry_skill.get(CONF_ID) or user_input[CONF_NAME] != entry_skill.get( CONF_NAME ): self.hass.config_entries.async_update_entry( self._entry, title=await async_config_entry_title(self.hass, self._data, self._options), data=self._data, ) return await self.async_step_done() return await self.async_step_expose_settings() data_schema = vol.Schema( { vol.Required(CONF_NAME, default=entry_skill.get(CONF_NAME)): TextSelector(), vol.Required(CONF_ID, default=entry_skill.get(CONF_ID)): TextSelector(), vol.Required(CONF_TOKEN, default=entry_skill.get(CONF_TOKEN)): TextSelector(), } ) if platform == SmartHomePlatform.VK: data_schema = vol.Schema( { vol.Required(CONF_NAME, default=entry_skill.get(CONF_NAME)): TextSelector(), vol.Required(CONF_ID, default=entry_skill.get(CONF_ID)): TextSelector(), } ) return self.async_show_form( step_id=f"skill_{platform}_cloud_plus", data_schema=data_schema, errors=errors, description_placeholders=description_placeholders, ) async def async_step_skill_yandex_cloud_plus(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose skill settings for cloud plus connection to the Yandex Smart Home platform.""" return await self._async_step_skill_cloud_plus(SmartHomePlatform.YANDEX, user_input) async def async_step_skill_vk_cloud_plus(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose skill settings for cloud plus connection to the VK Smart Home platform.""" return await self._async_step_skill_cloud_plus(SmartHomePlatform.VK, user_input) async def async_step_expose_settings(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose entity expose settings.""" if user_input is not None: self._options.update(user_input) match user_input[CONF_FILTER_SOURCE]: case EntityFilterSource.CONFIG_ENTRY: return await self.async_step_include_entities() case EntityFilterSource.GET_FROM_CONFIG_ENTRY: return await self.async_step_update_filter() case EntityFilterSource.LABEL: return await self.async_step_choose_label() return await self.async_step_done() return self.async_show_form( step_id="expose_settings", data_schema=vol.Schema( { vol.Required( CONF_FILTER_SOURCE, default=self._options.get(CONF_FILTER_SOURCE, EntityFilterSource.CONFIG_ENTRY), ): FILTER_SOURCE_SELECTOR, vol.Required( CONF_ENTRY_ALIASES, default=self._options.get(CONF_ENTRY_ALIASES, True), ): BooleanSelector(), } ), ) async def async_step_update_filter(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose a config entry from which the filter will be copied.""" if user_input is not None: if user_input.get(CONF_FILTER_SOURCE) is True: return await self.async_step_expose_settings() if entry := self.hass.config_entries.async_get_entry(user_input.get(CONF_ID, "")): self._options.update( { CONF_FILTER_SOURCE: EntityFilterSource.CONFIG_ENTRY, CONF_FILTER: entry.options[CONF_FILTER], } ) return await self.async_step_include_entities() config_entries = [ entry for entry in self.hass.config_entries.async_entries(DOMAIN) if CONF_FILTER in entry.options and (not self._entry or self._entry.entry_id != entry.entry_id) ] if not config_entries: data_schema = None if not self._entry: data_schema = vol.Schema({vol.Optional(CONF_FILTER_SOURCE): BooleanSelector()}) return self.async_show_form( step_id="update_filter", data_schema=data_schema, errors={"base": "missing_config_entry"}, ) return self.async_show_form( step_id="update_filter", data_schema=vol.Schema( { vol.Required(CONF_ID): SelectSelector( SelectSelectorConfig( mode=SelectSelectorMode.LIST, options=[ SelectOptionDict(value=entry.entry_id, label=entry.title) for entry in config_entries ], ), ) } ), ) async def async_step_include_entities(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose entities that should be exposed.""" errors = {} entities: set[str] = set() if entity_filter_config := self._options.get(CONF_FILTER): entities.update(entity_filter_config.get(CONF_INCLUDE_ENTITIES, [])) if len(entity_filter_config) > 1 or CONF_INCLUDE_ENTITIES not in entity_filter_config: entity_filter: EntityFilter = FILTER_SCHEMA(entity_filter_config) if not entity_filter.empty_filter: entities.update([s.entity_id for s in self.hass.states.async_all() if entity_filter(s.entity_id)]) if user_input is not None: if user_input[CONF_ENTITIES]: self._options[CONF_FILTER] = {CONF_INCLUDE_ENTITIES: user_input[CONF_ENTITIES]} return await self.async_step_done() else: errors["base"] = "entities_not_selected" entities.clear() return self.async_show_form( step_id="include_entities", data_schema=vol.Schema( { vol.Required(CONF_ENTITIES, default=sorted(entities)): selector.EntitySelector( selector.EntitySelectorConfig(multiple=True) ) } ), errors=errors, ) async def async_step_choose_label(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose a label that should be used as filter for entities.""" if user_input is not None: self._options.update(user_input) return await self.async_step_done() return self.async_show_form( step_id="choose_label", data_schema=vol.Schema( { vol.Required(CONF_LABEL, default=self._options.get(CONF_LABEL, "")): LabelSelector( LabelSelectorConfig(multiple=False), ) } ), ) async def async_step_done(self, _: ConfigType | None = None) -> ConfigFlowResult: """Finish the flow.""" raise NotImplementedError @callback def _get_direct_connection_entry(self, platform: SmartHomePlatform, user_id: str) -> ConfigEntry | None: """Return already configured config entry with direct connection.""" for entry in self.hass.config_entries.async_entries(DOMAIN): if self._entry and self._entry.entry_id == entry.entry_id: continue if CONF_SKILL in entry.options: if ( ConnectionType.DIRECT == entry.data[CONF_CONNECTION_TYPE] and platform == entry.data[CONF_PLATFORM] and user_id == entry.options[CONF_SKILL][CONF_USER_ID] ): return entry return None def _get_external_url(self) -> str: """Return external URL or abort the flow.""" try: return network.get_url(self.hass, allow_internal=False) except network.NoURLAvailableError: raise AbortFlow("missing_external_url") class ConfigFlowHandler(BaseFlowHandler, ConfigFlow, domain=DOMAIN): """Handle a config flow for Yandex Smart Home.""" VERSION = 6 def __init__(self) -> None: """Initialize a config flow handler.""" super().__init__() self._data: ConfigType = {} async def async_step_user(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: return await self.async_step_connection_type() return self.async_show_form(step_id="user") async def async_step_connection_type(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose connection type.""" errors = {} if user_input is not None: self._data.update(user_input) if user_input[CONF_CONNECTION_TYPE] == ConnectionType.CLOUD: try: instance = await cloud.register_instance(self.hass) self._data[CONF_CLOUD_INSTANCE] = { CONF_CLOUD_INSTANCE_ID: instance.id, CONF_CLOUD_INSTANCE_PASSWORD: instance.password, CONF_CLOUD_INSTANCE_CONNECTION_TOKEN: instance.connection_token, } except (ClientConnectorError, ClientResponseError): errors["base"] = "cannot_connect" _LOGGER.exception("Failed to register instance in Yandex Smart Home cloud") if not errors: match user_input[CONF_CONNECTION_TYPE]: case ConnectionType.DIRECT: return await self.async_step_platform_direct() case ConnectionType.CLOUD_PLUS: return await self.async_step_platform_cloud_plus() return await self.async_step_expose_settings() return self.async_show_form( step_id="connection_type", data_schema=vol.Schema( {vol.Required(CONF_CONNECTION_TYPE, default=ConnectionType.CLOUD): CONNECTION_TYPE_SELECTOR} ), errors=errors, ) async def async_step_platform_direct(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose smart home platform for direct connection.""" if user_input is not None: self._data.update(user_input) step_fn = getattr(self, f"async_step_skill_{self._data[CONF_PLATFORM]}_direct") return cast(ConfigFlowResult, await step_fn()) return self.async_show_form( step_id="platform_direct", description_placeholders={"external_url": self._get_external_url()}, data_schema=vol.Schema({vol.Required(CONF_PLATFORM): PLATFORM_SELECTOR}), ) async def async_step_platform_cloud_plus(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose smart home platform for cloud p connection.""" errors = {} if user_input is not None: self._data.update(user_input) try: instance = await cloud.register_instance(self.hass, SmartHomePlatform(user_input[CONF_PLATFORM])) self._data[CONF_CLOUD_INSTANCE] = { CONF_CLOUD_INSTANCE_ID: instance.id, CONF_CLOUD_INSTANCE_PASSWORD: instance.password, CONF_CLOUD_INSTANCE_CONNECTION_TOKEN: instance.connection_token, } except (ClientConnectorError, ClientResponseError): errors["base"] = "cannot_connect" _LOGGER.exception("Failed to register instance in Yandex Smart Home cloud") if not errors: step_fn = getattr(self, f"async_step_skill_{self._data[CONF_PLATFORM]}_cloud_plus") return cast(ConfigFlowResult, await step_fn()) return self.async_show_form( step_id="platform_cloud_plus", data_schema=vol.Schema({vol.Required(CONF_PLATFORM): PLATFORM_SELECTOR}), errors=errors, ) async def async_step_done(self, _: ConfigType | None = None) -> ConfigFlowResult: """Finish the flow.""" description = self._data[CONF_CONNECTION_TYPE] description_placeholders: dict[str, str] = self._data.get(CONF_CLOUD_INSTANCE, {}).copy() if self._data[CONF_CONNECTION_TYPE] in (ConnectionType.DIRECT, ConnectionType.CLOUD_PLUS): description += f"_{self._data[CONF_PLATFORM]}" if self._data[CONF_CONNECTION_TYPE] == ConnectionType.CLOUD_PLUS: description_placeholders[CONF_SKILL] = self._options[CONF_SKILL][CONF_NAME] if self._data[CONF_CONNECTION_TYPE] in (ConnectionType.CLOUD, ConnectionType.CLOUD_PLUS): description_placeholders[CONF_CLOUD_INSTANCE_OTP] = "-" try: description_placeholders[CONF_CLOUD_INSTANCE_OTP] = await cloud.get_instance_otp( self.hass, self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_ID], self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_CONNECTION_TOKEN], ) except Exception: _LOGGER.exception("Failed to get one time password for cloud connection") return self.async_create_entry( title=await async_config_entry_title(self.hass, self._data, self._options), description=description, description_placeholders=description_placeholders, data=self._data, options=self._options, ) @staticmethod @callback def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) class OptionsFlowHandler(OptionsFlow, BaseFlowHandler): """Handle a options flow for Yandex Smart Home.""" def __init__(self, entry: ConfigEntry): """Initialize an options flow handler.""" super().__init__() self._entry: ConfigEntry = entry self._data: ConfigType = entry.data.copy() self._options: ConfigType = entry.options.copy() async def async_step_init(self, _: ConfigType | None = None) -> ConfigFlowResult: """Show menu.""" options = ["expose_settings"] match self._data[CONF_CONNECTION_TYPE]: case ConnectionType.CLOUD: options += ["cloud_credentials", "context_user"] case ConnectionType.CLOUD_PLUS: options += ["cloud_credentials", f"skill_{self._data[CONF_PLATFORM]}_cloud_plus", "context_user"] case ConnectionType.DIRECT: options += [f"skill_{self._data[CONF_PLATFORM]}_direct"] options += ["maintenance"] return self.async_show_menu(step_id="init", menu_options=options) async def async_step_cloud_credentials(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Show cloud connection credentials.""" errors = {} if user_input is not None: return await self.async_step_init() description_placeholders = { CONF_SKILL: "Yaha Cloud", CONF_CLOUD_INSTANCE_ID: self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_ID], CONF_CLOUD_INSTANCE_PASSWORD: self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_PASSWORD], CONF_CLOUD_INSTANCE_OTP: "-", } if self._data[CONF_CONNECTION_TYPE] == ConnectionType.CLOUD_PLUS: description_placeholders[CONF_SKILL] = self._options[CONF_SKILL][CONF_NAME] try: description_placeholders[CONF_CLOUD_INSTANCE_OTP] = await cloud.get_instance_otp( self.hass, self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_ID], self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_CONNECTION_TOKEN], ) except Exception: errors["base"] = "cannot_connect" _LOGGER.exception("Failed to get one time password for cloud connection") return self.async_show_form( step_id="cloud_credentials", description_placeholders=description_placeholders, errors=errors ) async def async_step_context_user(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Choose user for a service calls context.""" if user_input is not None: if user_input[CONF_USER_ID] == USER_NONE: self._options.pop(CONF_USER_ID, None) else: self._options.update(user_input) return await self.async_step_done() return self.async_show_form( step_id="context_user", data_schema=vol.Schema( { vol.Required( CONF_USER_ID, default=self._options.get(CONF_USER_ID, USER_NONE) ): await _async_get_user_selector(self.hass) } ), ) async def async_step_maintenance(self, user_input: ConfigType | None = None) -> ConfigFlowResult: """Show maintenance actions.""" errors: dict[str, str] = {} description_placeholders = {} component: YandexSmartHome = self.hass.data[DOMAIN] entity_filter = component.get_entity_filter_from_yaml() if user_input is not None: if user_input.get(MaintenanceAction.REVOKE_OAUTH_TOKENS): match self._data[CONF_CONNECTION_TYPE]: case ConnectionType.CLOUD: try: await cloud.revoke_oauth_tokens( self.hass, self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_ID], self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_CONNECTION_TOKEN], ) except Exception as e: errors[MaintenanceAction.REVOKE_OAUTH_TOKENS] = "unknown" description_placeholders["error"] = str(e) case ConnectionType.DIRECT: errors[MaintenanceAction.REVOKE_OAUTH_TOKENS] = "manual_revoke_oauth_tokens" if user_input.get(MaintenanceAction.UNLINK_ALL_PLATFORMS): self._data[CONF_LINKED_PLATFORMS] = [] self.hass.config_entries.async_update_entry(self._entry, data=self._data) if user_input.get(MaintenanceAction.RESET_CLOUD_INSTANCE_CONNECTION_TOKEN): try: instance = await cloud.reset_connection_token( self.hass, self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_ID], self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_CONNECTION_TOKEN], ) self._data[CONF_CLOUD_INSTANCE] = { CONF_CLOUD_INSTANCE_ID: instance.id, CONF_CLOUD_INSTANCE_PASSWORD: self._data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_PASSWORD], CONF_CLOUD_INSTANCE_CONNECTION_TOKEN: instance.connection_token, } self.hass.config_entries.async_update_entry(self._entry, data=self._data) except Exception as e: errors[MaintenanceAction.RESET_CLOUD_INSTANCE_CONNECTION_TOKEN] = "unknown" description_placeholders["error"] = str(e) if user_input.get(MaintenanceAction.TRANSFER_ENTITY_FILTER_FROM_YAML): entity_ids: set[str] = set() if entity_filter: for state in self.hass.states.async_all(): if entity_filter(state.entity_id): entity_ids.add(state.entity_id) match self._options[CONF_FILTER_SOURCE]: case EntityFilterSource.CONFIG_ENTRY: entity_ids.update(self._options[CONF_FILTER][CONF_INCLUDE_ENTITIES]) self._options[CONF_FILTER] = {CONF_INCLUDE_ENTITIES: sorted(entity_ids)} case EntityFilterSource.LABEL: for entity_id in entity_ids: registry = er.async_get(self.hass) if entity := registry.async_get(entity_id): registry.async_update_entity( entity.entity_id, labels=entity.labels | {self._options[CONF_LABEL]}, ) if not errors: return await self.async_step_done() actions = [MaintenanceAction.REVOKE_OAUTH_TOKENS, MaintenanceAction.UNLINK_ALL_PLATFORMS] if self._data[CONF_CONNECTION_TYPE] in (ConnectionType.CLOUD, ConnectionType.CLOUD_PLUS): actions += [MaintenanceAction.RESET_CLOUD_INSTANCE_CONNECTION_TOKEN] if entity_filter and self._options[CONF_FILTER_SOURCE] in [ EntityFilterSource.CONFIG_ENTRY, EntityFilterSource.LABEL, ]: actions += [MaintenanceAction.TRANSFER_ENTITY_FILTER_FROM_YAML] return self.async_show_form( step_id="maintenance", data_schema=vol.Schema({vol.Optional(action.value): BooleanSelector() for action in actions}), errors=errors, description_placeholders=description_placeholders, ) async def async_step_done(self, _: ConfigType | None = None) -> ConfigFlowResult: """Finish the flow.""" return self.async_create_entry(data=self._options) async def _async_get_user_selector( hass: HomeAssistant, mode: SelectSelectorMode = SelectSelectorMode.LIST, required: bool = False ) -> SelectSelector: """Return user selector.""" users: list[SelectOptionDict] = [] if not required: users.append(SelectOptionDict(value=USER_NONE, label=USER_NONE)) for user in await hass.auth.async_get_users(): if any(gr.id == GROUP_ID_READ_ONLY for gr in user.groups): continue users.append(SelectOptionDict(value=user.id, label=user.name or user.id)) return SelectSelector( SelectSelectorConfig( mode=mode, translation_key=CONF_USER_ID, options=users, ), ) async def async_config_entry_title(hass: HomeAssistant, data: ConfigType, options: ConfigType) -> str: """Return config entry title.""" if data.get(CONF_CONNECTION_TYPE) == ConnectionType.CLOUD: instance_id = data[CONF_CLOUD_INSTANCE][CONF_CLOUD_INSTANCE_ID] return f"Yaha Cloud ({instance_id[:8]})" title = DEFAULT_CONFIG_ENTRY_TITLE connection_type = "" match data.get(CONF_CONNECTION_TYPE): case ConnectionType.CLOUD_PLUS: connection_type = "Cloud Plus" case ConnectionType.DIRECT: connection_type = "Direct" match data.get(CONF_PLATFORM): case SmartHomePlatform.YANDEX: title = f"Yandex Smart Home: {connection_type}" case SmartHomePlatform.VK: title = f"Marusia: {connection_type}" if skill := options.get(CONF_SKILL): parts: list[str] = [] if user := await hass.auth.async_get_user(skill.get(CONF_USER_ID, "")): parts.append(user.name or user.id[:6]) if skill_id := skill.get(CONF_ID, ""): parts.append(skill_id[:8]) if parts: title += f' ({" / ".join(parts)})' return title