mirror of
https://github.com/home-assistant/core.git
synced 2025-04-28 03:07:50 +00:00
Require newly configured esphome device to allow Home Assistant service calls (#95143)
* Require esphome service calls to be enabled For existing devices, calling Home Assistant services continues to be allowed. For newly configured devices, it must now be enabled in the options flow * fix * adjust * coverage * adjust * fix test * Update homeassistant/components/esphome/strings.json Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/esphome/strings.json Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/esphome/strings.json Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/esphome/__init__.py Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/esphome/__init__.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update homeassistant/components/esphome/__init__.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --------- Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
f4756fe1f9
commit
85d6e03dd3
@ -49,7 +49,11 @@ from homeassistant.helpers.template import Template
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .bluetooth import async_connect_scanner
|
from .bluetooth import async_connect_scanner
|
||||||
from .const import DOMAIN
|
from .const import (
|
||||||
|
CONF_ALLOW_SERVICE_CALLS,
|
||||||
|
DEFAULT_ALLOW_SERVICE_CALLS,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from .dashboard import async_get_dashboard, async_setup as async_setup_dashboard
|
from .dashboard import async_get_dashboard, async_setup as async_setup_dashboard
|
||||||
from .domain_data import DomainData
|
from .domain_data import DomainData
|
||||||
|
|
||||||
@ -154,11 +158,16 @@ async def async_setup_entry( # noqa: C901
|
|||||||
noise_psk=noise_psk,
|
noise_psk=noise_psk,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
services_issue = f"service_calls_not_enabled-{entry.unique_id}"
|
||||||
|
if entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS):
|
||||||
|
async_delete_issue(hass, DOMAIN, services_issue)
|
||||||
|
|
||||||
domain_data = DomainData.get(hass)
|
domain_data = DomainData.get(hass)
|
||||||
entry_data = RuntimeEntryData(
|
entry_data = RuntimeEntryData(
|
||||||
client=cli,
|
client=cli,
|
||||||
entry_id=entry.entry_id,
|
entry_id=entry.entry_id,
|
||||||
store=domain_data.get_or_create_store(hass, entry),
|
store=domain_data.get_or_create_store(hass, entry),
|
||||||
|
original_options=dict(entry.options),
|
||||||
)
|
)
|
||||||
domain_data.set_entry_data(entry, entry_data)
|
domain_data.set_entry_data(entry, entry_data)
|
||||||
|
|
||||||
@ -177,6 +186,8 @@ async def async_setup_entry( # noqa: C901
|
|||||||
@callback
|
@callback
|
||||||
def async_on_service_call(service: HomeassistantServiceCall) -> None:
|
def async_on_service_call(service: HomeassistantServiceCall) -> None:
|
||||||
"""Call service when user automation in ESPHome config is triggered."""
|
"""Call service when user automation in ESPHome config is triggered."""
|
||||||
|
device_info = entry_data.device_info
|
||||||
|
assert device_info is not None
|
||||||
domain, service_name = service.service.split(".", 1)
|
domain, service_name = service.service.split(".", 1)
|
||||||
service_data = service.data
|
service_data = service.data
|
||||||
|
|
||||||
@ -194,7 +205,7 @@ async def async_setup_entry( # noqa: C901
|
|||||||
return
|
return
|
||||||
|
|
||||||
if service.is_event:
|
if service.is_event:
|
||||||
# ESPHome uses servicecall packet for both events and service calls
|
# ESPHome uses service call packet for both events and service calls
|
||||||
# Ensure the user can only send events of form 'esphome.xyz'
|
# Ensure the user can only send events of form 'esphome.xyz'
|
||||||
if domain != "esphome":
|
if domain != "esphome":
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@ -215,12 +226,34 @@ async def async_setup_entry( # noqa: C901
|
|||||||
**service_data,
|
**service_data,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
elif entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS):
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.services.async_call(
|
hass.services.async_call(
|
||||||
domain, service_name, service_data, blocking=True
|
domain, service_name, service_data, blocking=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
services_issue,
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="service_calls_not_allowed",
|
||||||
|
translation_placeholders={
|
||||||
|
"name": device_info.friendly_name or device_info.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_LOGGER.error(
|
||||||
|
"%s: Service call %s.%s: with data %s rejected; "
|
||||||
|
"If you trust this device and want to allow access for it to make "
|
||||||
|
"Home Assistant service calls, you can enable this "
|
||||||
|
"functionality in the options flow",
|
||||||
|
device_info.friendly_name or device_info.name,
|
||||||
|
domain,
|
||||||
|
service_name,
|
||||||
|
service_data,
|
||||||
|
)
|
||||||
|
|
||||||
async def _send_home_assistant_state(
|
async def _send_home_assistant_state(
|
||||||
entity_id: str, attribute: str | None, state: State | None
|
entity_id: str, attribute: str | None, state: State | None
|
||||||
@ -449,6 +482,8 @@ async def async_setup_entry( # noqa: C901
|
|||||||
await reconnect_logic.start()
|
await reconnect_logic.start()
|
||||||
entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback)
|
entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback)
|
||||||
|
|
||||||
|
entry.async_on_unload(entry.add_update_listener(entry_data.async_update_listener))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,14 +20,19 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components import dhcp, zeroconf
|
from homeassistant.components import dhcp, zeroconf
|
||||||
from homeassistant.components.hassio import HassioServiceInfo
|
from homeassistant.components.hassio import HassioServiceInfo
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
|
||||||
from . import CONF_DEVICE_NAME, CONF_NOISE_PSK
|
from . import CONF_DEVICE_NAME, CONF_NOISE_PSK
|
||||||
from .const import DOMAIN
|
from .const import (
|
||||||
|
CONF_ALLOW_SERVICE_CALLS,
|
||||||
|
DEFAULT_ALLOW_SERVICE_CALLS,
|
||||||
|
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from .dashboard import async_get_dashboard, async_set_dashboard_info
|
from .dashboard import async_get_dashboard, async_set_dashboard_info
|
||||||
|
|
||||||
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||||
@ -237,6 +242,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_NOISE_PSK: self._noise_psk or "",
|
CONF_NOISE_PSK: self._noise_psk or "",
|
||||||
CONF_DEVICE_NAME: self._device_name,
|
CONF_DEVICE_NAME: self._device_name,
|
||||||
}
|
}
|
||||||
|
config_options = {
|
||||||
|
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||||
|
}
|
||||||
if self._reauth_entry:
|
if self._reauth_entry:
|
||||||
entry = self._reauth_entry
|
entry = self._reauth_entry
|
||||||
self.hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
@ -253,6 +261,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self._name,
|
title=self._name,
|
||||||
data=config_data,
|
data=config_data,
|
||||||
|
options=config_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_encryption_key(
|
async def async_step_encryption_key(
|
||||||
@ -388,3 +397,38 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
self._noise_psk = noise_psk
|
self._noise_psk = noise_psk
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
) -> OptionsFlowHandler:
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(OptionsFlow):
|
||||||
|
"""Handle a option flow for esphome."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_ALLOW_SERVICE_CALLS,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
|
||||||
|
),
|
||||||
|
): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
"""ESPHome constants."""
|
"""ESPHome constants."""
|
||||||
|
|
||||||
DOMAIN = "esphome"
|
DOMAIN = "esphome"
|
||||||
|
|
||||||
|
CONF_ALLOW_SERVICE_CALLS = "allow_service_calls"
|
||||||
|
DEFAULT_ALLOW_SERVICE_CALLS = True
|
||||||
|
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||||
|
@ -109,6 +109,7 @@ class RuntimeEntryData:
|
|||||||
entity_info_callbacks: dict[
|
entity_info_callbacks: dict[
|
||||||
type[EntityInfo], list[Callable[[list[EntityInfo]], None]]
|
type[EntityInfo], list[Callable[[list[EntityInfo]], None]]
|
||||||
] = field(default_factory=dict)
|
] = field(default_factory=dict)
|
||||||
|
original_options: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -365,3 +366,11 @@ class RuntimeEntryData:
|
|||||||
return store_data
|
return store_data
|
||||||
|
|
||||||
self.store.async_delay_save(_memorized_storage, SAVE_DELAY)
|
self.store.async_delay_save(_memorized_storage, SAVE_DELAY)
|
||||||
|
|
||||||
|
async def async_update_listener(
|
||||||
|
self, hass: HomeAssistant, entry: ConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
if self.original_options == entry.options:
|
||||||
|
return
|
||||||
|
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
|
||||||
|
@ -46,6 +46,15 @@
|
|||||||
},
|
},
|
||||||
"flow_title": "{name}"
|
"flow_title": "{name}"
|
||||||
},
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"allow_service_calls": "Allow the device to make Home Assistant service calls."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"assist_in_progress": {
|
"assist_in_progress": {
|
||||||
@ -69,6 +78,10 @@
|
|||||||
"api_password_deprecated": {
|
"api_password_deprecated": {
|
||||||
"title": "API Password deprecated on {name}",
|
"title": "API Password deprecated on {name}",
|
||||||
"description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue."
|
"description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue."
|
||||||
|
},
|
||||||
|
"service_calls_not_allowed": {
|
||||||
|
"title": "{name} is not permitted to call Home Assistant services",
|
||||||
|
"description": "The ESPHome device attempted to make a Home Assistant service call, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to make Home Assistant service calls, you can enable this functionality in the options flow."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,10 @@ from homeassistant.components.esphome import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
dashboard,
|
dashboard,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.esphome.const import (
|
||||||
|
CONF_ALLOW_SERVICE_CALLS,
|
||||||
|
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -173,6 +177,9 @@ async def _mock_generic_device_entry(
|
|||||||
CONF_PORT: 6053,
|
CONF_PORT: 6053,
|
||||||
CONF_PASSWORD: "",
|
CONF_PASSWORD: "",
|
||||||
},
|
},
|
||||||
|
options={
|
||||||
|
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS
|
||||||
|
},
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
mock_device = MockESPHomeDevice(entry)
|
mock_device = MockESPHomeDevice(entry)
|
||||||
@ -208,7 +215,7 @@ async def _mock_generic_device_entry(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
with patch.object(ReconnectLogic, "_try_connect", mock_try_connect):
|
with patch.object(ReconnectLogic, "_try_connect", mock_try_connect):
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await try_connect_done.wait()
|
await try_connect_done.wait()
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from aioesphomeapi import (
|
from aioesphomeapi import (
|
||||||
|
APIClient,
|
||||||
APIConnectionError,
|
APIConnectionError,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
InvalidAuthAPIError,
|
InvalidAuthAPIError,
|
||||||
@ -20,6 +21,10 @@ from homeassistant.components.esphome import (
|
|||||||
DomainData,
|
DomainData,
|
||||||
dashboard,
|
dashboard,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.esphome.const import (
|
||||||
|
CONF_ALLOW_SERVICE_CALLS,
|
||||||
|
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||||
|
)
|
||||||
from homeassistant.components.hassio import HassioServiceInfo
|
from homeassistant.components.hassio import HassioServiceInfo
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -32,7 +37,7 @@ from tests.common import MockConfigEntry
|
|||||||
INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM="
|
INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM="
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=False)
|
||||||
def mock_setup_entry():
|
def mock_setup_entry():
|
||||||
"""Mock setting up a config entry."""
|
"""Mock setting up a config entry."""
|
||||||
with patch("homeassistant.components.esphome.async_setup_entry", return_value=True):
|
with patch("homeassistant.components.esphome.async_setup_entry", return_value=True):
|
||||||
@ -40,7 +45,7 @@ def mock_setup_entry():
|
|||||||
|
|
||||||
|
|
||||||
async def test_user_connection_works(
|
async def test_user_connection_works(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we can finish a config flow."""
|
"""Test we can finish a config flow."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -66,6 +71,9 @@ async def test_user_connection_works(
|
|||||||
CONF_NOISE_PSK: "",
|
CONF_NOISE_PSK: "",
|
||||||
CONF_DEVICE_NAME: "test",
|
CONF_DEVICE_NAME: "test",
|
||||||
}
|
}
|
||||||
|
assert result["options"] == {
|
||||||
|
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS
|
||||||
|
}
|
||||||
assert result["title"] == "test"
|
assert result["title"] == "test"
|
||||||
assert result["result"].unique_id == "11:22:33:44:55:aa"
|
assert result["result"].unique_id == "11:22:33:44:55:aa"
|
||||||
|
|
||||||
@ -79,7 +87,7 @@ async def test_user_connection_works(
|
|||||||
|
|
||||||
|
|
||||||
async def test_user_connection_updates_host(
|
async def test_user_connection_updates_host(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test setup up the same name updates the host."""
|
"""Test setup up the same name updates the host."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@ -108,7 +116,7 @@ async def test_user_connection_updates_host(
|
|||||||
|
|
||||||
|
|
||||||
async def test_user_resolve_error(
|
async def test_user_resolve_error(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user step with IP resolve error."""
|
"""Test user step with IP resolve error."""
|
||||||
|
|
||||||
@ -133,7 +141,7 @@ async def test_user_resolve_error(
|
|||||||
|
|
||||||
|
|
||||||
async def test_user_connection_error(
|
async def test_user_connection_error(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user step with connection error."""
|
"""Test user step with connection error."""
|
||||||
mock_client.device_info.side_effect = APIConnectionError
|
mock_client.device_info.side_effect = APIConnectionError
|
||||||
@ -154,7 +162,7 @@ async def test_user_connection_error(
|
|||||||
|
|
||||||
|
|
||||||
async def test_user_with_password(
|
async def test_user_with_password(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user step with password."""
|
"""Test user step with password."""
|
||||||
mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
|
mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
|
||||||
@ -210,7 +218,7 @@ async def test_user_invalid_password(
|
|||||||
|
|
||||||
|
|
||||||
async def test_login_connection_error(
|
async def test_login_connection_error(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user step with connection error on login attempt."""
|
"""Test user step with connection error on login attempt."""
|
||||||
mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
|
mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
|
||||||
@ -236,7 +244,7 @@ async def test_login_connection_error(
|
|||||||
|
|
||||||
|
|
||||||
async def test_discovery_initiation(
|
async def test_discovery_initiation(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test discovery importing works."""
|
"""Test discovery importing works."""
|
||||||
service_info = zeroconf.ZeroconfServiceInfo(
|
service_info = zeroconf.ZeroconfServiceInfo(
|
||||||
@ -268,7 +276,7 @@ async def test_discovery_initiation(
|
|||||||
|
|
||||||
|
|
||||||
async def test_discovery_no_mac(
|
async def test_discovery_no_mac(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test discovery aborted if old ESPHome without mac in zeroconf."""
|
"""Test discovery aborted if old ESPHome without mac in zeroconf."""
|
||||||
service_info = zeroconf.ZeroconfServiceInfo(
|
service_info = zeroconf.ZeroconfServiceInfo(
|
||||||
@ -287,7 +295,9 @@ async def test_discovery_no_mac(
|
|||||||
assert flow["reason"] == "mdns_missing_mac"
|
assert flow["reason"] == "mdns_missing_mac"
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_already_configured(hass: HomeAssistant, mock_client) -> None:
|
async def test_discovery_already_configured(
|
||||||
|
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
|
||||||
|
) -> None:
|
||||||
"""Test discovery aborts if already configured via hostname."""
|
"""Test discovery aborts if already configured via hostname."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@ -314,7 +324,9 @@ async def test_discovery_already_configured(hass: HomeAssistant, mock_client) ->
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_duplicate_data(hass: HomeAssistant, mock_client) -> None:
|
async def test_discovery_duplicate_data(
|
||||||
|
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
|
||||||
|
) -> None:
|
||||||
"""Test discovery aborts if same mDNS packet arrives."""
|
"""Test discovery aborts if same mDNS packet arrives."""
|
||||||
service_info = zeroconf.ZeroconfServiceInfo(
|
service_info = zeroconf.ZeroconfServiceInfo(
|
||||||
host="192.168.43.183",
|
host="192.168.43.183",
|
||||||
@ -339,7 +351,9 @@ async def test_discovery_duplicate_data(hass: HomeAssistant, mock_client) -> Non
|
|||||||
assert result["reason"] == "already_in_progress"
|
assert result["reason"] == "already_in_progress"
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_updates_unique_id(hass: HomeAssistant, mock_client) -> None:
|
async def test_discovery_updates_unique_id(
|
||||||
|
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
|
||||||
|
) -> None:
|
||||||
"""Test a duplicate discovery host aborts and updates existing entry."""
|
"""Test a duplicate discovery host aborts and updates existing entry."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@ -369,7 +383,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant, mock_client) ->
|
|||||||
|
|
||||||
|
|
||||||
async def test_user_requires_psk(
|
async def test_user_requires_psk(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user step with requiring encryption key."""
|
"""Test user step with requiring encryption key."""
|
||||||
mock_client.device_info.side_effect = RequiresEncryptionAPIError
|
mock_client.device_info.side_effect = RequiresEncryptionAPIError
|
||||||
@ -390,7 +404,7 @@ async def test_user_requires_psk(
|
|||||||
|
|
||||||
|
|
||||||
async def test_encryption_key_valid_psk(
|
async def test_encryption_key_valid_psk(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test encryption key step with valid key."""
|
"""Test encryption key step with valid key."""
|
||||||
|
|
||||||
@ -424,7 +438,7 @@ async def test_encryption_key_valid_psk(
|
|||||||
|
|
||||||
|
|
||||||
async def test_encryption_key_invalid_psk(
|
async def test_encryption_key_invalid_psk(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test encryption key step with invalid key."""
|
"""Test encryption key step with invalid key."""
|
||||||
|
|
||||||
@ -473,7 +487,7 @@ async def test_reauth_initiation(
|
|||||||
|
|
||||||
|
|
||||||
async def test_reauth_confirm_valid(
|
async def test_reauth_confirm_valid(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth initiation with valid PSK."""
|
"""Test reauth initiation with valid PSK."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@ -502,7 +516,11 @@ async def test_reauth_confirm_valid(
|
|||||||
|
|
||||||
|
|
||||||
async def test_reauth_fixed_via_dashboard(
|
async def test_reauth_fixed_via_dashboard(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_dashboard
|
hass: HomeAssistant,
|
||||||
|
mock_client,
|
||||||
|
mock_zeroconf: None,
|
||||||
|
mock_dashboard,
|
||||||
|
mock_setup_entry: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth fixed automatically via dashboard."""
|
"""Test reauth fixed automatically via dashboard."""
|
||||||
|
|
||||||
@ -554,6 +572,7 @@ async def test_reauth_fixed_via_dashboard_add_encryption_remove_password(
|
|||||||
mock_zeroconf: None,
|
mock_zeroconf: None,
|
||||||
mock_dashboard,
|
mock_dashboard,
|
||||||
mock_config_entry,
|
mock_config_entry,
|
||||||
|
mock_setup_entry: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth fixed automatically via dashboard with password removed."""
|
"""Test reauth fixed automatically via dashboard with password removed."""
|
||||||
mock_client.device_info.side_effect = (
|
mock_client.device_info.side_effect = (
|
||||||
@ -596,6 +615,7 @@ async def test_reauth_fixed_via_remove_password(
|
|||||||
mock_client,
|
mock_client,
|
||||||
mock_config_entry,
|
mock_config_entry,
|
||||||
mock_dashboard,
|
mock_dashboard,
|
||||||
|
mock_setup_entry: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth fixed automatically by seeing password removed."""
|
"""Test reauth fixed automatically by seeing password removed."""
|
||||||
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
|
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
|
||||||
@ -615,7 +635,11 @@ async def test_reauth_fixed_via_remove_password(
|
|||||||
|
|
||||||
|
|
||||||
async def test_reauth_fixed_via_dashboard_at_confirm(
|
async def test_reauth_fixed_via_dashboard_at_confirm(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_dashboard
|
hass: HomeAssistant,
|
||||||
|
mock_client,
|
||||||
|
mock_zeroconf: None,
|
||||||
|
mock_dashboard,
|
||||||
|
mock_setup_entry: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth fixed automatically via dashboard at confirm step."""
|
"""Test reauth fixed automatically via dashboard at confirm step."""
|
||||||
|
|
||||||
@ -668,7 +692,7 @@ async def test_reauth_fixed_via_dashboard_at_confirm(
|
|||||||
|
|
||||||
|
|
||||||
async def test_reauth_confirm_invalid(
|
async def test_reauth_confirm_invalid(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth initiation with invalid PSK."""
|
"""Test reauth initiation with invalid PSK."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@ -709,7 +733,7 @@ async def test_reauth_confirm_invalid(
|
|||||||
|
|
||||||
|
|
||||||
async def test_reauth_confirm_invalid_with_unique_id(
|
async def test_reauth_confirm_invalid_with_unique_id(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test reauth initiation with invalid PSK."""
|
"""Test reauth initiation with invalid PSK."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@ -750,7 +774,9 @@ async def test_reauth_confirm_invalid_with_unique_id(
|
|||||||
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
|
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_dhcp_updates_host(hass: HomeAssistant, mock_client) -> None:
|
async def test_discovery_dhcp_updates_host(
|
||||||
|
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
|
||||||
|
) -> None:
|
||||||
"""Test dhcp discovery updates host and aborts."""
|
"""Test dhcp discovery updates host and aborts."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@ -774,7 +800,9 @@ async def test_discovery_dhcp_updates_host(hass: HomeAssistant, mock_client) ->
|
|||||||
assert entry.data[CONF_HOST] == "192.168.43.184"
|
assert entry.data[CONF_HOST] == "192.168.43.184"
|
||||||
|
|
||||||
|
|
||||||
async def test_discovery_dhcp_no_changes(hass: HomeAssistant, mock_client) -> None:
|
async def test_discovery_dhcp_no_changes(
|
||||||
|
hass: HomeAssistant, mock_client: APIClient, mock_setup_entry: None
|
||||||
|
) -> None:
|
||||||
"""Test dhcp discovery updates host and aborts."""
|
"""Test dhcp discovery updates host and aborts."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@ -827,7 +855,11 @@ async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_encryption_key_via_dashboard(
|
async def test_zeroconf_encryption_key_via_dashboard(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_dashboard
|
hass: HomeAssistant,
|
||||||
|
mock_client,
|
||||||
|
mock_zeroconf: None,
|
||||||
|
mock_dashboard,
|
||||||
|
mock_setup_entry: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test encryption key retrieved from dashboard."""
|
"""Test encryption key retrieved from dashboard."""
|
||||||
service_info = zeroconf.ZeroconfServiceInfo(
|
service_info = zeroconf.ZeroconfServiceInfo(
|
||||||
@ -889,7 +921,11 @@ async def test_zeroconf_encryption_key_via_dashboard(
|
|||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_no_encryption_key_via_dashboard(
|
async def test_zeroconf_no_encryption_key_via_dashboard(
|
||||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_dashboard
|
hass: HomeAssistant,
|
||||||
|
mock_client,
|
||||||
|
mock_zeroconf: None,
|
||||||
|
mock_dashboard,
|
||||||
|
mock_setup_entry: None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test encryption key not retrieved from dashboard."""
|
"""Test encryption key not retrieved from dashboard."""
|
||||||
service_info = zeroconf.ZeroconfServiceInfo(
|
service_info = zeroconf.ZeroconfServiceInfo(
|
||||||
@ -920,3 +956,42 @@ async def test_zeroconf_no_encryption_key_via_dashboard(
|
|||||||
|
|
||||||
assert result["type"] == FlowResultType.FORM
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["step_id"] == "encryption_key"
|
assert result["step_id"] == "encryption_key"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("option_value", [True, False])
|
||||||
|
async def test_option_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
option_value: bool,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_generic_device_entry,
|
||||||
|
) -> None:
|
||||||
|
"""Test config flow options."""
|
||||||
|
entry = await mock_generic_device_entry(
|
||||||
|
mock_client=mock_client,
|
||||||
|
entity_info=[],
|
||||||
|
user_service=[],
|
||||||
|
states=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
assert result["data_schema"]({}) == {
|
||||||
|
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.esphome.async_setup_entry", return_value=True
|
||||||
|
) as mock_reload:
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
CONF_ALLOW_SERVICE_CALLS: option_value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: option_value}
|
||||||
|
assert len(mock_reload.mock_calls) == int(option_value)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user