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>pull/95220/head
parent
f4756fe1f9
commit
85d6e03dd3
|
@ -49,7 +49,11 @@ from homeassistant.helpers.template import Template
|
|||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
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 .domain_data import DomainData
|
||||
|
||||
|
@ -154,11 +158,16 @@ async def async_setup_entry( # noqa: C901
|
|||
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)
|
||||
entry_data = RuntimeEntryData(
|
||||
client=cli,
|
||||
entry_id=entry.entry_id,
|
||||
store=domain_data.get_or_create_store(hass, entry),
|
||||
original_options=dict(entry.options),
|
||||
)
|
||||
domain_data.set_entry_data(entry, entry_data)
|
||||
|
||||
|
@ -177,6 +186,8 @@ async def async_setup_entry( # noqa: C901
|
|||
@callback
|
||||
def async_on_service_call(service: HomeassistantServiceCall) -> None:
|
||||
"""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)
|
||||
service_data = service.data
|
||||
|
||||
|
@ -194,7 +205,7 @@ async def async_setup_entry( # noqa: C901
|
|||
return
|
||||
|
||||
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'
|
||||
if domain != "esphome":
|
||||
_LOGGER.error(
|
||||
|
@ -215,12 +226,34 @@ async def async_setup_entry( # noqa: C901
|
|||
**service_data,
|
||||
},
|
||||
)
|
||||
else:
|
||||
elif entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS):
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(
|
||||
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(
|
||||
entity_id: str, attribute: str | None, state: State | None
|
||||
|
@ -449,6 +482,8 @@ async def async_setup_entry( # noqa: C901
|
|||
await reconnect_logic.start()
|
||||
entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(entry_data.async_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -20,14 +20,19 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components import dhcp, zeroconf
|
||||
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.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
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
|
||||
|
||||
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||
|
@ -237,6 +242,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
CONF_NOISE_PSK: self._noise_psk or "",
|
||||
CONF_DEVICE_NAME: self._device_name,
|
||||
}
|
||||
config_options = {
|
||||
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
}
|
||||
if self._reauth_entry:
|
||||
entry = self._reauth_entry
|
||||
self.hass.config_entries.async_update_entry(
|
||||
|
@ -253,6 +261,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
return self.async_create_entry(
|
||||
title=self._name,
|
||||
data=config_data,
|
||||
options=config_options,
|
||||
)
|
||||
|
||||
async def async_step_encryption_key(
|
||||
|
@ -388,3 +397,38 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
self._noise_psk = noise_psk
|
||||
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."""
|
||||
|
||||
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[
|
||||
type[EntityInfo], list[Callable[[list[EntityInfo]], None]]
|
||||
] = field(default_factory=dict)
|
||||
original_options: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -365,3 +366,11 @@ class RuntimeEntryData:
|
|||
return store_data
|
||||
|
||||
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}"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"allow_service_calls": "Allow the device to make Home Assistant service calls."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"assist_in_progress": {
|
||||
|
@ -69,6 +78,10 @@
|
|||
"api_password_deprecated": {
|
||||
"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."
|
||||
},
|
||||
"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,
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
@ -173,6 +177,9 @@ async def _mock_generic_device_entry(
|
|||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
},
|
||||
options={
|
||||
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
mock_device = MockESPHomeDevice(entry)
|
||||
|
@ -208,7 +215,7 @@ async def _mock_generic_device_entry(
|
|||
return result
|
||||
|
||||
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 hass.async_block_till_done()
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIConnectionError,
|
||||
DeviceInfo,
|
||||
InvalidAuthAPIError,
|
||||
|
@ -20,6 +21,10 @@ from homeassistant.components.esphome import (
|
|||
DomainData,
|
||||
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.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -32,7 +37,7 @@ from tests.common import MockConfigEntry
|
|||
INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM="
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@pytest.fixture(autouse=False)
|
||||
def mock_setup_entry():
|
||||
"""Mock setting up a config entry."""
|
||||
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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||
) -> None:
|
||||
"""Test we can finish a config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -66,6 +71,9 @@ async def test_user_connection_works(
|
|||
CONF_NOISE_PSK: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
}
|
||||
assert result["options"] == {
|
||||
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS
|
||||
}
|
||||
assert result["title"] == "test"
|
||||
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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||
) -> None:
|
||||
"""Test setup up the same name updates the host."""
|
||||
entry = MockConfigEntry(
|
||||
|
@ -108,7 +116,7 @@ async def test_user_connection_updates_host(
|
|||
|
||||
|
||||
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:
|
||||
"""Test user step with IP resolve error."""
|
||||
|
||||
|
@ -133,7 +141,7 @@ async def test_user_resolve_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:
|
||||
"""Test user step with connection error."""
|
||||
mock_client.device_info.side_effect = APIConnectionError
|
||||
|
@ -154,7 +162,7 @@ async def test_user_connection_error(
|
|||
|
||||
|
||||
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:
|
||||
"""Test user step with password."""
|
||||
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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||
) -> None:
|
||||
"""Test user step with connection error on login attempt."""
|
||||
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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||
) -> None:
|
||||
"""Test discovery importing works."""
|
||||
service_info = zeroconf.ZeroconfServiceInfo(
|
||||
|
@ -268,7 +276,7 @@ async def test_discovery_initiation(
|
|||
|
||||
|
||||
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:
|
||||
"""Test discovery aborted if old ESPHome without mac in zeroconf."""
|
||||
service_info = zeroconf.ZeroconfServiceInfo(
|
||||
|
@ -287,7 +295,9 @@ async def test_discovery_no_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."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
|
@ -314,7 +324,9 @@ async def test_discovery_already_configured(hass: HomeAssistant, mock_client) ->
|
|||
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."""
|
||||
service_info = zeroconf.ZeroconfServiceInfo(
|
||||
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"
|
||||
|
||||
|
||||
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."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
|
@ -369,7 +383,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant, mock_client) ->
|
|||
|
||||
|
||||
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:
|
||||
"""Test user step with requiring encryption key."""
|
||||
mock_client.device_info.side_effect = RequiresEncryptionAPIError
|
||||
|
@ -390,7 +404,7 @@ async def test_user_requires_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:
|
||||
"""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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||
) -> None:
|
||||
"""Test encryption key step with invalid key."""
|
||||
|
||||
|
@ -473,7 +487,7 @@ async def test_reauth_initiation(
|
|||
|
||||
|
||||
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:
|
||||
"""Test reauth initiation with valid PSK."""
|
||||
entry = MockConfigEntry(
|
||||
|
@ -502,7 +516,11 @@ async def test_reauth_confirm_valid(
|
|||
|
||||
|
||||
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:
|
||||
"""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_dashboard,
|
||||
mock_config_entry,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test reauth fixed automatically via dashboard with password removed."""
|
||||
mock_client.device_info.side_effect = (
|
||||
|
@ -596,6 +615,7 @@ async def test_reauth_fixed_via_remove_password(
|
|||
mock_client,
|
||||
mock_config_entry,
|
||||
mock_dashboard,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test reauth fixed automatically by seeing password removed."""
|
||||
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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_dashboard
|
||||
hass: HomeAssistant,
|
||||
mock_client,
|
||||
mock_zeroconf: None,
|
||||
mock_dashboard,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
|
||||
) -> None:
|
||||
"""Test reauth initiation with invalid PSK."""
|
||||
entry = MockConfigEntry(
|
||||
|
@ -709,7 +733,7 @@ async def test_reauth_confirm_invalid(
|
|||
|
||||
|
||||
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:
|
||||
"""Test reauth initiation with invalid PSK."""
|
||||
entry = MockConfigEntry(
|
||||
|
@ -750,7 +774,9 @@ async def test_reauth_confirm_invalid_with_unique_id(
|
|||
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."""
|
||||
entry = MockConfigEntry(
|
||||
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"
|
||||
|
||||
|
||||
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."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
|
@ -827,7 +855,11 @@ async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None:
|
|||
|
||||
|
||||
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:
|
||||
"""Test encryption key retrieved from dashboard."""
|
||||
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(
|
||||
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_dashboard
|
||||
hass: HomeAssistant,
|
||||
mock_client,
|
||||
mock_zeroconf: None,
|
||||
mock_dashboard,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test encryption key not retrieved from dashboard."""
|
||||
service_info = zeroconf.ZeroconfServiceInfo(
|
||||
|
@ -920,3 +956,42 @@ async def test_zeroconf_no_encryption_key_via_dashboard(
|
|||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
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…
Reference in New Issue