mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Add preview to sensor group config and option flows (#83638)
This commit is contained in:
parent
52b1e34af0
commit
b885dfa5a8
@ -7,8 +7,10 @@ from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.const import CONF_ENTITIES, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
@ -22,6 +24,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
from . import DOMAIN
|
||||
from .binary_sensor import CONF_ALL
|
||||
from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC
|
||||
from .sensor import SensorGroup
|
||||
|
||||
_STATISTIC_MEASURES = [
|
||||
"min",
|
||||
@ -36,15 +39,22 @@ _STATISTIC_MEASURES = [
|
||||
|
||||
|
||||
async def basic_group_options_schema(
|
||||
domain: str | list[str], handler: SchemaCommonFlowHandler
|
||||
domain: str | list[str], handler: SchemaCommonFlowHandler | None
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
if handler is None:
|
||||
entity_selector = selector.selector(
|
||||
{"entity": {"domain": domain, "multiple": True}}
|
||||
)
|
||||
else:
|
||||
entity_selector = entity_selector_without_own_entities(
|
||||
cast(SchemaOptionsFlowHandler, handler.parent_handler),
|
||||
selector.EntitySelectorConfig(domain=domain, multiple=True),
|
||||
)
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITIES): entity_selector_without_own_entities(
|
||||
cast(SchemaOptionsFlowHandler, handler.parent_handler),
|
||||
selector.EntitySelectorConfig(domain=domain, multiple=True),
|
||||
),
|
||||
vol.Required(CONF_ENTITIES): entity_selector,
|
||||
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
@ -96,7 +106,7 @@ SENSOR_OPTIONS = {
|
||||
|
||||
|
||||
async def sensor_options_schema(
|
||||
domain: str, handler: SchemaCommonFlowHandler
|
||||
domain: str, handler: SchemaCommonFlowHandler | None
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
return (
|
||||
@ -184,6 +194,7 @@ CONFIG_FLOW = {
|
||||
"sensor": SchemaFlowFormStep(
|
||||
SENSOR_CONFIG_SCHEMA,
|
||||
validate_user_input=set_group_type("sensor"),
|
||||
preview="group_sensor",
|
||||
),
|
||||
"switch": SchemaFlowFormStep(
|
||||
basic_group_config_schema("switch"),
|
||||
@ -202,7 +213,10 @@ OPTIONS_FLOW = {
|
||||
"media_player": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "media_player")
|
||||
),
|
||||
"sensor": SchemaFlowFormStep(partial(sensor_options_schema, "sensor")),
|
||||
"sensor": SchemaFlowFormStep(
|
||||
partial(sensor_options_schema, "sensor"),
|
||||
preview="group_sensor",
|
||||
),
|
||||
"switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")),
|
||||
}
|
||||
|
||||
@ -241,6 +255,12 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
)
|
||||
_async_hide_members(hass, options[CONF_ENTITIES], hidden_by)
|
||||
|
||||
@callback
|
||||
@staticmethod
|
||||
def async_setup_preview(hass: HomeAssistant) -> None:
|
||||
"""Set up preview WS API."""
|
||||
websocket_api.async_register_command(hass, ws_preview_sensor)
|
||||
|
||||
|
||||
def _async_hide_members(
|
||||
hass: HomeAssistant, members: list[str], hidden_by: er.RegistryEntryHider | None
|
||||
@ -253,3 +273,56 @@ def _async_hide_members(
|
||||
if entity_id not in registry.entities:
|
||||
continue
|
||||
registry.async_update_entity(entity_id, hidden_by=hidden_by)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "group/sensor/start_preview",
|
||||
vol.Required("flow_id"): str,
|
||||
vol.Required("flow_type"): vol.Any("config_flow", "options_flow"),
|
||||
vol.Required("user_input"): dict,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_preview_sensor(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Generate a preview."""
|
||||
if msg["flow_type"] == "config_flow":
|
||||
validated = SENSOR_CONFIG_SCHEMA(msg["user_input"])
|
||||
ignore_non_numeric = False
|
||||
name = validated["name"]
|
||||
else:
|
||||
validated = (await sensor_options_schema("sensor", None))(msg["user_input"])
|
||||
flow_status = hass.config_entries.options.async_get(msg["flow_id"])
|
||||
config_entry = hass.config_entries.async_get_entry(flow_status["handler"])
|
||||
if not config_entry:
|
||||
raise HomeAssistantError
|
||||
ignore_non_numeric = validated[CONF_IGNORE_NON_NUMERIC]
|
||||
name = config_entry.options["name"]
|
||||
|
||||
@callback
|
||||
def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None:
|
||||
"""Forward config entry state events to websocket."""
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg["id"], {"state": state, "attributes": attributes}
|
||||
)
|
||||
)
|
||||
|
||||
sensor = SensorGroup(
|
||||
None,
|
||||
name,
|
||||
validated[CONF_ENTITIES],
|
||||
ignore_non_numeric,
|
||||
validated[CONF_TYPE],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
sensor.hass = hass
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
connection.subscriptions[msg["id"]] = sensor.async_start_preview(
|
||||
async_preview_updated
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Platform allowing several sensors to be grouped into one sensor to provide numeric combinations."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import statistics
|
||||
@ -33,7 +33,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import (
|
||||
@ -303,6 +303,26 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
self._state_incorrect: set[str] = set()
|
||||
self._extra_state_attribute: dict[str, Any] = {}
|
||||
|
||||
@callback
|
||||
def async_start_preview(
|
||||
self,
|
||||
preview_callback: Callable[[str, Mapping[str, Any]], None],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Render a preview."""
|
||||
|
||||
@callback
|
||||
def async_state_changed_listener(
|
||||
event: EventType[EventStateChangedData] | None,
|
||||
) -> None:
|
||||
"""Handle child updates."""
|
||||
self.async_update_group_state()
|
||||
preview_callback(*self._async_generate_attributes())
|
||||
|
||||
async_state_changed_listener(None)
|
||||
return async_track_state_change_event(
|
||||
self.hass, self._entity_ids, async_state_changed_listener
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
|
||||
|
@ -1814,6 +1814,14 @@ class ConfigFlow(data_entry_flow.FlowHandler):
|
||||
class OptionsFlowManager(data_entry_flow.FlowManager):
|
||||
"""Flow to set options for a configuration entry."""
|
||||
|
||||
def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry:
|
||||
"""Return config entry or raise if not found."""
|
||||
entry = self.hass.config_entries.async_get_entry(config_entry_id)
|
||||
if entry is None:
|
||||
raise UnknownEntry(config_entry_id)
|
||||
|
||||
return entry
|
||||
|
||||
async def async_create_flow(
|
||||
self,
|
||||
handler_key: str,
|
||||
@ -1825,10 +1833,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager):
|
||||
|
||||
Entry_id and flow.handler is the same thing to map entry with flow.
|
||||
"""
|
||||
entry = self.hass.config_entries.async_get_entry(handler_key)
|
||||
if entry is None:
|
||||
raise UnknownEntry(handler_key)
|
||||
|
||||
entry = self._async_get_config_entry(handler_key)
|
||||
handler = await _async_get_flow_handler(self.hass, entry.domain, {})
|
||||
return handler.async_get_options_flow(entry)
|
||||
|
||||
@ -1853,6 +1858,14 @@ class OptionsFlowManager(data_entry_flow.FlowManager):
|
||||
result["result"] = True
|
||||
return result
|
||||
|
||||
async def _async_setup_preview(self, flow: data_entry_flow.FlowHandler) -> None:
|
||||
"""Set up preview for an option flow handler."""
|
||||
entry = self._async_get_config_entry(flow.handler)
|
||||
await _load_integration(self.hass, entry.domain, {})
|
||||
if entry.domain not in self._preview:
|
||||
self._preview.add(entry.domain)
|
||||
flow.async_setup_preview(self.hass)
|
||||
|
||||
|
||||
class OptionsFlow(data_entry_flow.FlowHandler):
|
||||
"""Base class for config options flows."""
|
||||
@ -2016,15 +2029,9 @@ async def support_remove_from_device(hass: HomeAssistant, domain: str) -> bool:
|
||||
return hasattr(component, "async_remove_config_entry_device")
|
||||
|
||||
|
||||
async def _async_get_flow_handler(
|
||||
async def _load_integration(
|
||||
hass: HomeAssistant, domain: str, hass_config: ConfigType
|
||||
) -> type[ConfigFlow]:
|
||||
"""Get a flow handler for specified domain."""
|
||||
|
||||
# First check if there is a handler registered for the domain
|
||||
if domain in hass.config.components and (handler := HANDLERS.get(domain)):
|
||||
return handler
|
||||
|
||||
) -> None:
|
||||
try:
|
||||
integration = await loader.async_get_integration(hass, domain)
|
||||
except loader.IntegrationNotFound as err:
|
||||
@ -2044,6 +2051,18 @@ async def _async_get_flow_handler(
|
||||
)
|
||||
raise data_entry_flow.UnknownHandler
|
||||
|
||||
|
||||
async def _async_get_flow_handler(
|
||||
hass: HomeAssistant, domain: str, hass_config: ConfigType
|
||||
) -> type[ConfigFlow]:
|
||||
"""Get a flow handler for specified domain."""
|
||||
|
||||
# First check if there is a handler registered for the domain
|
||||
if domain in hass.config.components and (handler := HANDLERS.get(domain)):
|
||||
return handler
|
||||
|
||||
await _load_integration(hass, domain, hass_config)
|
||||
|
||||
if handler := HANDLERS.get(domain):
|
||||
return handler
|
||||
|
||||
|
@ -95,6 +95,7 @@ class FlowResult(TypedDict, total=False):
|
||||
last_step: bool | None
|
||||
menu_options: list[str] | dict[str, str]
|
||||
options: Mapping[str, Any]
|
||||
preview: str | None
|
||||
progress_action: str
|
||||
reason: str
|
||||
required: bool
|
||||
@ -135,6 +136,7 @@ class FlowManager(abc.ABC):
|
||||
) -> None:
|
||||
"""Initialize the flow manager."""
|
||||
self.hass = hass
|
||||
self._preview: set[str] = set()
|
||||
self._progress: dict[str, FlowHandler] = {}
|
||||
self._handler_progress_index: dict[str, set[str]] = {}
|
||||
self._init_data_process_index: dict[type, set[str]] = {}
|
||||
@ -395,6 +397,10 @@ class FlowManager(abc.ABC):
|
||||
flow.flow_id, flow.handler, err.reason, err.description_placeholders
|
||||
)
|
||||
|
||||
# Setup the flow handler's preview if needed
|
||||
if result.get("preview") is not None:
|
||||
await self._async_setup_preview(flow)
|
||||
|
||||
if not isinstance(result["type"], FlowResultType):
|
||||
result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable]
|
||||
report(
|
||||
@ -429,6 +435,12 @@ class FlowManager(abc.ABC):
|
||||
|
||||
return result
|
||||
|
||||
async def _async_setup_preview(self, flow: FlowHandler) -> None:
|
||||
"""Set up preview for a flow handler."""
|
||||
if flow.handler not in self._preview:
|
||||
self._preview.add(flow.handler)
|
||||
flow.async_setup_preview(self.hass)
|
||||
|
||||
|
||||
class FlowHandler:
|
||||
"""Handle a data entry flow."""
|
||||
@ -504,6 +516,7 @@ class FlowHandler:
|
||||
errors: dict[str, str] | None = None,
|
||||
description_placeholders: Mapping[str, str | None] | None = None,
|
||||
last_step: bool | None = None,
|
||||
preview: str | None = None,
|
||||
) -> FlowResult:
|
||||
"""Return the definition of a form to gather user input."""
|
||||
return FlowResult(
|
||||
@ -515,6 +528,7 @@ class FlowHandler:
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
last_step=last_step, # Display next or submit button in frontend
|
||||
preview=preview, # Display preview component in frontend
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -635,6 +649,11 @@ class FlowHandler:
|
||||
def async_remove(self) -> None:
|
||||
"""Notification that the flow has been removed."""
|
||||
|
||||
@callback
|
||||
@staticmethod
|
||||
def async_setup_preview(hass: HomeAssistant) -> None:
|
||||
"""Set up preview."""
|
||||
|
||||
|
||||
@callback
|
||||
def _create_abort_data(
|
||||
|
@ -756,31 +756,10 @@ class Entity(ABC):
|
||||
return f"{device_name} {name}" if device_name else name
|
||||
|
||||
@callback
|
||||
def _async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine."""
|
||||
if self._platform_state == EntityPlatformState.REMOVED:
|
||||
# Polling returned after the entity has already been removed
|
||||
return
|
||||
|
||||
hass = self.hass
|
||||
entity_id = self.entity_id
|
||||
def _async_generate_attributes(self) -> tuple[str, dict[str, Any]]:
|
||||
"""Calculate state string and attribute mapping."""
|
||||
entry = self.registry_entry
|
||||
|
||||
if entry and entry.disabled_by:
|
||||
if not self._disabled_reported:
|
||||
self._disabled_reported = True
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Entity %s is incorrectly being triggered for updates while it"
|
||||
" is disabled. This is a bug in the %s integration"
|
||||
),
|
||||
entity_id,
|
||||
self.platform.platform_name,
|
||||
)
|
||||
return
|
||||
|
||||
start = timer()
|
||||
|
||||
attr = self.capability_attributes
|
||||
attr = dict(attr) if attr else {}
|
||||
|
||||
@ -818,6 +797,33 @@ class Entity(ABC):
|
||||
if (supported_features := self.supported_features) is not None:
|
||||
attr[ATTR_SUPPORTED_FEATURES] = supported_features
|
||||
|
||||
return (state, attr)
|
||||
|
||||
@callback
|
||||
def _async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine."""
|
||||
if self._platform_state == EntityPlatformState.REMOVED:
|
||||
# Polling returned after the entity has already been removed
|
||||
return
|
||||
|
||||
hass = self.hass
|
||||
entity_id = self.entity_id
|
||||
|
||||
if (entry := self.registry_entry) and entry.disabled_by:
|
||||
if not self._disabled_reported:
|
||||
self._disabled_reported = True
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Entity %s is incorrectly being triggered for updates while it"
|
||||
" is disabled. This is a bug in the %s integration"
|
||||
),
|
||||
entity_id,
|
||||
self.platform.platform_name,
|
||||
)
|
||||
return
|
||||
|
||||
start = timer()
|
||||
state, attr = self._async_generate_attributes()
|
||||
end = timer()
|
||||
|
||||
if end - start > 0.4 and not self._slow_reported:
|
||||
|
@ -78,6 +78,9 @@ class SchemaFlowFormStep(SchemaFlowStep):
|
||||
have priority over the suggested values.
|
||||
"""
|
||||
|
||||
preview: str | None = None
|
||||
"""Optional preview component."""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SchemaFlowMenuStep(SchemaFlowStep):
|
||||
@ -237,6 +240,7 @@ class SchemaCommonFlowHandler:
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
last_step=last_step,
|
||||
preview=form_step.preview,
|
||||
)
|
||||
|
||||
async def _async_menu_step(
|
||||
@ -271,7 +275,10 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC):
|
||||
raise UnknownHandler
|
||||
|
||||
return SchemaOptionsFlowHandler(
|
||||
config_entry, cls.options_flow, cls.async_options_flow_finished
|
||||
config_entry,
|
||||
cls.options_flow,
|
||||
cls.async_options_flow_finished,
|
||||
cls.async_setup_preview,
|
||||
)
|
||||
|
||||
# Create an async_get_options_flow method
|
||||
@ -285,6 +292,11 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC):
|
||||
"""Initialize config flow."""
|
||||
self._common_handler = SchemaCommonFlowHandler(self, self.config_flow, None)
|
||||
|
||||
@callback
|
||||
@staticmethod
|
||||
def async_setup_preview(hass: HomeAssistant) -> None:
|
||||
"""Set up preview."""
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_supports_options_flow(
|
||||
@ -357,6 +369,7 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
|
||||
options_flow: Mapping[str, SchemaFlowStep],
|
||||
async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None]
|
||||
| None = None,
|
||||
async_setup_preview: Callable[[HomeAssistant], None] | None = None,
|
||||
) -> None:
|
||||
"""Initialize options flow.
|
||||
|
||||
@ -378,6 +391,9 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
|
||||
types.MethodType(self._async_step(step), self),
|
||||
)
|
||||
|
||||
if async_setup_preview:
|
||||
setattr(self, "async_setup_preview", async_setup_preview)
|
||||
|
||||
@staticmethod
|
||||
def _async_step(step_id: str) -> Callable:
|
||||
"""Generate a step handler."""
|
||||
|
@ -123,6 +123,7 @@ async def test_legacy_subscription_repair_flow(
|
||||
"errors": None,
|
||||
"description_placeholders": None,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
@ -205,6 +206,7 @@ async def test_legacy_subscription_repair_flow_timeout(
|
||||
"errors": None,
|
||||
"description_placeholders": None,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
with patch("homeassistant.components.cloud.repairs.MAX_RETRIES", new=0):
|
||||
|
@ -396,6 +396,7 @@ async def test_initialize_flow(hass: HomeAssistant, client) -> None:
|
||||
},
|
||||
"errors": {"username": "Should be unique."},
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
|
||||
@ -571,6 +572,7 @@ async def test_two_step_flow(
|
||||
"description_placeholders": None,
|
||||
"errors": None,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
with patch.dict(HANDLERS, {"test": TestFlow}):
|
||||
@ -647,6 +649,7 @@ async def test_continue_flow_unauth(
|
||||
"description_placeholders": None,
|
||||
"errors": None,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
hass_admin_user.groups = []
|
||||
@ -822,6 +825,7 @@ async def test_options_flow(hass: HomeAssistant, client) -> None:
|
||||
"description_placeholders": {"enabled": "Set to true to be true"},
|
||||
"errors": None,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
|
||||
@ -917,6 +921,7 @@ async def test_two_step_options_flow(hass: HomeAssistant, client) -> None:
|
||||
"description_placeholders": None,
|
||||
"errors": None,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
with patch.dict(HANDLERS, {"test": TestFlow}):
|
||||
@ -998,6 +1003,7 @@ async def test_options_flow_with_invalid_data(hass: HomeAssistant, client) -> No
|
||||
"description_placeholders": None,
|
||||
"errors": None,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
with patch.dict(HANDLERS, {"test": TestFlow}):
|
||||
|
@ -9,6 +9,7 @@
|
||||
'flow_id': <ANY>,
|
||||
'handler': 'gardena_bluetooth',
|
||||
'last_step': None,
|
||||
'preview': None,
|
||||
'step_id': 'confirm',
|
||||
'type': <FlowResultType.FORM: 'form'>,
|
||||
})
|
||||
@ -136,6 +137,7 @@
|
||||
'flow_id': <ANY>,
|
||||
'handler': 'gardena_bluetooth',
|
||||
'last_step': None,
|
||||
'preview': None,
|
||||
'step_id': 'user',
|
||||
'type': <FlowResultType.FORM: 'form'>,
|
||||
})
|
||||
@ -150,6 +152,7 @@
|
||||
'flow_id': <ANY>,
|
||||
'handler': 'gardena_bluetooth',
|
||||
'last_step': None,
|
||||
'preview': None,
|
||||
'step_id': 'confirm',
|
||||
'type': <FlowResultType.FORM: 'form'>,
|
||||
})
|
||||
@ -198,6 +201,7 @@
|
||||
'flow_id': <ANY>,
|
||||
'handler': 'gardena_bluetooth',
|
||||
'last_step': None,
|
||||
'preview': None,
|
||||
'step_id': 'user',
|
||||
'type': <FlowResultType.FORM: 'form'>,
|
||||
})
|
||||
@ -212,6 +216,7 @@
|
||||
'flow_id': <ANY>,
|
||||
'handler': 'gardena_bluetooth',
|
||||
'last_step': None,
|
||||
'preview': None,
|
||||
'step_id': 'confirm',
|
||||
'type': <FlowResultType.FORM: 'form'>,
|
||||
})
|
||||
|
@ -10,6 +10,7 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -446,3 +447,174 @@ async def test_options_flow_hides_members(
|
||||
|
||||
assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by
|
||||
assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by
|
||||
|
||||
|
||||
async def test_config_flow_sensor_preview(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test the config flow preview."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
input_sensors = ["sensor.input_one", "sensor.input_two"]
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "sensor"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "sensor"
|
||||
assert result["errors"] is None
|
||||
assert result["preview"] == "group_sensor"
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "group/sensor/start_preview",
|
||||
"flow_id": result["flow_id"],
|
||||
"flow_type": "config_flow",
|
||||
"user_input": {
|
||||
"name": "My sensor group",
|
||||
"entities": input_sensors,
|
||||
"type": "max",
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] is None
|
||||
|
||||
msg = await client.receive_json()
|
||||
assert msg["event"] == {
|
||||
"attributes": {
|
||||
"friendly_name": "My sensor group",
|
||||
"icon": "mdi:calculator",
|
||||
},
|
||||
"state": "unavailable",
|
||||
}
|
||||
|
||||
hass.states.async_set("sensor.input_one", "10")
|
||||
hass.states.async_set("sensor.input_two", "20")
|
||||
|
||||
msg = await client.receive_json()
|
||||
assert msg["event"] == {
|
||||
"attributes": {
|
||||
"entity_id": ["sensor.input_one", "sensor.input_two"],
|
||||
"friendly_name": "My sensor group",
|
||||
"icon": "mdi:calculator",
|
||||
"max_entity_id": "sensor.input_two",
|
||||
},
|
||||
"state": "20.0",
|
||||
}
|
||||
|
||||
|
||||
async def test_option_flow_sensor_preview(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test the option flow preview."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"entities": ["sensor.input_one", "sensor.input_two"],
|
||||
"group_type": "sensor",
|
||||
"hide_members": False,
|
||||
"name": "My sensor group",
|
||||
"type": "min",
|
||||
},
|
||||
title="My min_max",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
input_sensors = ["sensor.input_one", "sensor.input_two"]
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
assert result["preview"] == "group_sensor"
|
||||
|
||||
hass.states.async_set("sensor.input_one", "10")
|
||||
hass.states.async_set("sensor.input_two", "20")
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "group/sensor/start_preview",
|
||||
"flow_id": result["flow_id"],
|
||||
"flow_type": "options_flow",
|
||||
"user_input": {
|
||||
"entities": input_sensors,
|
||||
"type": "min",
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] is None
|
||||
|
||||
msg = await client.receive_json()
|
||||
assert msg["event"] == {
|
||||
"attributes": {
|
||||
"entity_id": ["sensor.input_one", "sensor.input_two"],
|
||||
"friendly_name": "My sensor group",
|
||||
"icon": "mdi:calculator",
|
||||
"min_entity_id": "sensor.input_one",
|
||||
},
|
||||
"state": "10.0",
|
||||
}
|
||||
|
||||
|
||||
async def test_option_flow_sensor_preview_config_entry_removed(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test the option flow preview where the config entry is removed."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"entities": ["sensor.input_one", "sensor.input_two"],
|
||||
"group_type": "sensor",
|
||||
"hide_members": False,
|
||||
"name": "My sensor group",
|
||||
"type": "min",
|
||||
},
|
||||
title="My min_max",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
input_sensors = ["sensor.input_one", "sensor.input_two"]
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
assert result["preview"] == "group_sensor"
|
||||
|
||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "group/sensor/start_preview",
|
||||
"flow_id": result["flow_id"],
|
||||
"flow_type": "options_flow",
|
||||
"user_input": {
|
||||
"entities": input_sensors,
|
||||
"type": "min",
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"] == {"code": "unknown_error", "message": "Unknown error"}
|
||||
|
@ -84,6 +84,7 @@ async def test_supervisor_issue_repair_flow(
|
||||
"errors": None,
|
||||
"description_placeholders": {"reference": "/dev/sda1"},
|
||||
"last_step": True,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
@ -292,6 +293,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir
|
||||
"errors": None,
|
||||
"description_placeholders": None,
|
||||
"last_step": True,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
@ -371,6 +373,7 @@ async def test_supervisor_issue_repair_flow_skip_confirmation(
|
||||
"errors": None,
|
||||
"description_placeholders": None,
|
||||
"last_step": True,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
@ -580,6 +583,7 @@ async def test_supervisor_issue_docker_config_repair_flow(
|
||||
"errors": None,
|
||||
"description_placeholders": {"components": "Home Assistant\n- test"},
|
||||
"last_step": True,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
|
@ -212,6 +212,7 @@ async def test_issues_created(
|
||||
"flow_id": ANY,
|
||||
"handler": DOMAIN,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
"step_id": "confirm",
|
||||
"type": "form",
|
||||
}
|
||||
|
@ -313,6 +313,7 @@ async def test_fix_issue(
|
||||
"flow_id": ANY,
|
||||
"handler": domain,
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
"step_id": step,
|
||||
"type": "form",
|
||||
}
|
||||
|
@ -264,6 +264,7 @@ async def test_pin_form_init(pin_form) -> None:
|
||||
"step_id": "pin",
|
||||
"type": "form",
|
||||
"last_step": None,
|
||||
"preview": None,
|
||||
}
|
||||
assert pin_form == expected
|
||||
|
||||
|
@ -1178,6 +1178,27 @@ async def test_entry_options_abort(
|
||||
)
|
||||
|
||||
|
||||
async def test_entry_options_unknown_config_entry(
|
||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||
) -> None:
|
||||
"""Test that we can abort options flow."""
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_entity_platform(hass, "config_flow.test", None)
|
||||
|
||||
class TestFlow:
|
||||
"""Test flow."""
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Test options flow."""
|
||||
|
||||
with pytest.raises(config_entries.UnknownEntry):
|
||||
await manager.options.async_create_flow(
|
||||
"blah", context={"source": "test"}, data=None
|
||||
)
|
||||
|
||||
|
||||
async def test_entry_setup_succeed(
|
||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||
) -> None:
|
||||
@ -3919,3 +3940,75 @@ async def test_task_tracking(hass: HomeAssistant) -> None:
|
||||
hass.loop.call_soon(event.set)
|
||||
await entry._async_process_on_unload(hass)
|
||||
assert results == ["on_unload", "background", "normal"]
|
||||
|
||||
|
||||
async def test_preview_supported(
|
||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||
) -> None:
|
||||
"""Test preview support."""
|
||||
|
||||
preview_calls = []
|
||||
|
||||
class MockFlowHandler(config_entries.ConfigFlow):
|
||||
"""Define a mock flow handler."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_test1(self, data):
|
||||
"""Mock Reauth."""
|
||||
return self.async_show_form(step_id="next")
|
||||
|
||||
async def async_step_test2(self, data):
|
||||
"""Mock Reauth."""
|
||||
return self.async_show_form(step_id="next", preview="test")
|
||||
|
||||
@callback
|
||||
@staticmethod
|
||||
def async_setup_preview(hass: HomeAssistant) -> None:
|
||||
"""Set up preview."""
|
||||
preview_calls.append(None)
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_entity_platform(hass, "config_flow.test", None)
|
||||
|
||||
assert len(preview_calls) == 0
|
||||
|
||||
with patch.dict(
|
||||
config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler}
|
||||
):
|
||||
result = await manager.flow.async_init("test", context={"source": "test1"})
|
||||
|
||||
assert len(preview_calls) == 0
|
||||
assert result["preview"] is None
|
||||
|
||||
result = await manager.flow.async_init("test", context={"source": "test2"})
|
||||
|
||||
assert len(preview_calls) == 1
|
||||
assert result["preview"] == "test"
|
||||
|
||||
|
||||
async def test_preview_not_supported(
|
||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||
) -> None:
|
||||
"""Test preview support."""
|
||||
|
||||
class MockFlowHandler(config_entries.ConfigFlow):
|
||||
"""Define a mock flow handler."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, data):
|
||||
"""Mock Reauth."""
|
||||
return self.async_show_form(step_id="user_confirm")
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_entity_platform(hass, "config_flow.test", None)
|
||||
|
||||
with patch.dict(
|
||||
config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler}
|
||||
):
|
||||
result = await manager.flow.async_init(
|
||||
"test", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["preview"] is None
|
||||
|
Loading…
x
Reference in New Issue
Block a user