This commit is contained in:
Victor Alexandrovich Tsyrenschikov
2026-03-30 20:25:42 +05:00
parent 139f9f1bd2
commit 373ed28445
2449 changed files with 53602 additions and 0 deletions

View File

@@ -0,0 +1,761 @@
"""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