Cleanup Plugwise config flow and tests (#65818)

This commit is contained in:
Franck Nijhof 2022-02-05 19:07:02 +01:00 committed by GitHub
parent 5621e20963
commit b0bb2d2453
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 299 deletions

View File

@ -2,13 +2,14 @@
from __future__ import annotations
import logging
from typing import Any
from plugwise.exceptions import InvalidAuthentication, PlugwiseException
from plugwise.smile import Smile
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.components import zeroconf
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import (
CONF_BASE,
CONF_HOST,
@ -17,6 +18,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -25,11 +27,8 @@ from .const import (
DEFAULT_PORT,
DEFAULT_USERNAME,
DOMAIN,
FLOW_NET,
FLOW_SMILE,
FLOW_STRETCH,
FLOW_TYPE,
FLOW_USB,
PW_TYPE,
SMILE,
STRETCH,
@ -39,14 +38,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
CONF_MANUAL_PATH = "Enter Manually"
CONNECTION_SCHEMA = vol.Schema(
{vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In([FLOW_NET, FLOW_USB])}
)
# PLACEHOLDER USB connection validation
def _base_gw_schema(discovery_info):
"""Generate base schema for gateways."""
@ -64,14 +55,13 @@ def _base_gw_schema(discovery_info):
return vol.Schema(base_gw_schema)
async def validate_gw_input(hass: core.HomeAssistant, data):
async def validate_gw_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
"""
Validate whether the user input allows us to connect to the gateway.
Data has the keys from _base_gw_schema() with values provided by the user.
"""
websession = async_get_clientsession(hass, verify_ssl=False)
api = Smile(
host=data[CONF_HOST],
password=data[CONF_PASSWORD],
@ -80,35 +70,25 @@ async def validate_gw_input(hass: core.HomeAssistant, data):
timeout=30,
websession=websession,
)
try:
await api.connect()
except InvalidAuthentication as err:
raise InvalidAuth from err
except PlugwiseException as err:
raise CannotConnect from err
await api.connect()
return api
class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Plugwise Smile."""
VERSION = 1
def __init__(self):
"""Initialize the Plugwise config flow."""
self.discovery_info: zeroconf.ZeroconfServiceInfo | None = None
self._username: str = DEFAULT_USERNAME
discovery_info: ZeroconfServiceInfo | None = None
_username: str = DEFAULT_USERNAME
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
self, discovery_info: ZeroconfServiceInfo
) -> FlowResult:
"""Prepare configuration for a discovered Plugwise Smile."""
self.discovery_info = discovery_info
_properties = discovery_info.properties
# unique_id is needed here, to be able to determine whether the discovered device is known, or not.
unique_id = discovery_info.hostname.split(".")[0]
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.host})
@ -125,18 +105,15 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_PORT: discovery_info.port,
CONF_USERNAME: self._username,
}
return await self.async_step_user_gateway()
return await self.async_step_user()
# PLACEHOLDER USB step_user
async def async_step_user_gateway(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step when using network/gateway setups."""
api = None
errors = {}
if user_input is not None:
user_input.pop(FLOW_TYPE, None)
if self.discovery_info:
user_input[CONF_HOST] = self.discovery_info.host
user_input[CONF_PORT] = self.discovery_info.port
@ -144,16 +121,14 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
api = await validate_gw_input(self.hass, user_input)
except CannotConnect:
errors[CONF_BASE] = "cannot_connect"
except InvalidAuth:
except InvalidAuthentication:
errors[CONF_BASE] = "invalid_auth"
except PlugwiseException:
errors[CONF_BASE] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors[CONF_BASE] = "unknown"
if not errors:
else:
await self.async_set_unique_id(
api.smile_hostname or api.gateway_id, raise_on_progress=False
)
@ -163,30 +138,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=api.smile_name, data=user_input)
return self.async_show_form(
step_id="user_gateway",
step_id="user",
data_schema=_base_gw_schema(self.discovery_info),
errors=errors,
)
async def async_step_user(self, user_input=None):
"""Handle the initial step when using network/gateway setups."""
errors = {}
if user_input is not None:
if user_input[FLOW_TYPE] == FLOW_NET:
return await self.async_step_user_gateway()
# PLACEHOLDER for USB_FLOW
return self.async_show_form(
step_id="user",
data_schema=CONNECTION_SCHEMA,
errors=errors,
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -8,11 +8,9 @@ ATTR_ILLUMINANCE = "illuminance"
COORDINATOR = "coordinator"
DEVICE_STATE = "device_state"
DOMAIN = "plugwise"
FLOW_NET = "Network: Smile/Stretch"
FLOW_SMILE = "smile (Adam/Anna/P1)"
FLOW_STRETCH = "stretch (Stretch)"
FLOW_TYPE = "flow_type"
FLOW_USB = "USB: Stick - Coming soon"
GATEWAY = "gateway"
PW_TYPE = "plugwise_type"
SCHEDULE_OFF = "false"

View File

@ -1,9 +1,11 @@
"""Setup mocks for the Plugwise integration tests."""
from __future__ import annotations
from collections.abc import Generator
from functools import partial
from http import HTTPStatus
import re
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import jsonpickle
from plugwise.exceptions import (
@ -24,16 +26,27 @@ def _read_json(environment, call):
return jsonpickle.decode(fixture)
@pytest.fixture(name="mock_smile")
def mock_smile():
"""Create a Mock Smile for testing exceptions."""
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Mock setting up a config entry."""
with patch(
"homeassistant.components.plugwise.async_setup_entry", return_value=True
) as mock_setup:
yield mock_setup
@pytest.fixture()
def mock_smile_config_flow() -> Generator[None, MagicMock, None]:
"""Return a mocked Smile client."""
with patch(
"homeassistant.components.plugwise.config_flow.Smile",
autospec=True,
) as smile_mock:
smile_mock.InvalidAuthentication = InvalidAuthentication
smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.return_value.connect.return_value = True
yield smile_mock.return_value
smile = smile_mock.return_value
smile.smile_hostname = "smile12345"
smile.smile_name = "Test Smile Name"
smile.connect.return_value = True
yield smile
@pytest.fixture(name="mock_smile_unauth")

View File

@ -1,5 +1,5 @@
"""Test the Plugwise config flow."""
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
from plugwise.exceptions import (
ConnectionFailedError,
@ -8,15 +8,8 @@ from plugwise.exceptions import (
)
import pytest
from homeassistant.components import zeroconf
from homeassistant.components.plugwise.const import (
API,
DEFAULT_PORT,
DOMAIN,
FLOW_NET,
FLOW_TYPE,
PW_TYPE,
)
from homeassistant.components.plugwise.const import API, DEFAULT_PORT, DOMAIN, PW_TYPE
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import (
CONF_HOST,
@ -26,7 +19,12 @@ from homeassistant.const import (
CONF_SOURCE,
CONF_USERNAME,
)
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from tests.common import MockConfigEntry
@ -38,7 +36,7 @@ TEST_PORT = 81
TEST_USERNAME = "smile"
TEST_USERNAME2 = "stretch"
TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo(
TEST_DISCOVERY = ZeroconfServiceInfo(
host=TEST_HOST,
hostname=f"{TEST_HOSTNAME}.local.",
name="mock_name",
@ -50,7 +48,8 @@ TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo(
},
type="mock_type",
)
TEST_DISCOVERY2 = zeroconf.ZeroconfServiceInfo(
TEST_DISCOVERY2 = ZeroconfServiceInfo(
host=TEST_HOST,
hostname=f"{TEST_HOSTNAME2}.local.",
name="mock_name",
@ -77,49 +76,32 @@ def mock_smile():
yield smile_mock.return_value
async def test_form_flow_gateway(hass):
"""Test we get the form for Plugwise Gateway product type."""
async def test_form(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_smile_config_flow: MagicMock,
) -> None:
"""Test the full user configuration flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
assert result["step_id"] == "user"
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("errors") == {}
assert result.get("step_id") == "user"
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={FLOW_TYPE: FLOW_NET}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
assert result["step_id"] == "user_gateway"
async def test_form(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.plugwise.config_flow.Smile.connect",
return_value=True,
), patch(
"homeassistant.components.plugwise.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["data"] == {
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result2.get("title") == "Test Smile Name"
assert result2.get("data") == {
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: DEFAULT_PORT,
@ -128,72 +110,79 @@ async def test_form(hass):
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smile_config_flow.connect.mock_calls) == 1
async def test_zeroconf_form(hass):
"""Test we get the form."""
@pytest.mark.parametrize(
"discovery,username",
[
(TEST_DISCOVERY, TEST_USERNAME),
(TEST_DISCOVERY2, TEST_USERNAME2),
],
)
async def test_zeroconf_flow(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_smile_config_flow: MagicMock,
discovery: ZeroconfServiceInfo,
username: str,
) -> None:
"""Test config flow for smile devices."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_ZEROCONF},
data=TEST_DISCOVERY,
data=discovery,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.plugwise.config_flow.Smile.connect",
return_value=True,
), patch(
"homeassistant.components.plugwise.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: TEST_PASSWORD},
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("errors") == {}
assert result.get("step_id") == "user"
assert "flow_id" in result
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: TEST_PASSWORD},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["data"] == {
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result2.get("title") == "Test Smile Name"
assert result2.get("data") == {
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: DEFAULT_PORT,
CONF_USERNAME: TEST_USERNAME,
CONF_USERNAME: username,
PW_TYPE: API,
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smile_config_flow.connect.mock_calls) == 1
async def test_zeroconf_stretch_form(hass):
"""Test we get the form."""
async def test_zeroconf_flow_stretch(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_smile_config_flow: MagicMock,
) -> None:
"""Test config flow for stretch devices."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_ZEROCONF},
data=TEST_DISCOVERY2,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.plugwise.config_flow.Smile.connect",
return_value=True,
), patch(
"homeassistant.components.plugwise.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: TEST_PASSWORD},
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("errors") == {}
assert result.get("step_id") == "user"
assert "flow_id" in result
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: TEST_PASSWORD},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["data"] == {
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result2.get("title") == "Test Smile Name"
assert result2.get("data") == {
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: DEFAULT_PORT,
@ -202,9 +191,10 @@ async def test_zeroconf_stretch_form(hass):
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smile_config_flow.connect.mock_calls) == 1
async def test_zercoconf_discovery_update_configuration(hass):
async def test_zercoconf_discovery_update_configuration(hass: HomeAssistant) -> None:
"""Test if a discovered device is configured and updated with new host."""
entry = MockConfigEntry(
domain=DOMAIN,
@ -222,154 +212,65 @@ async def test_zercoconf_discovery_update_configuration(hass):
data=TEST_DISCOVERY,
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert result.get("type") == RESULT_TYPE_ABORT
assert result.get("reason") == "already_configured"
assert entry.data[CONF_HOST] == "1.1.1.1"
async def test_form_username(hass):
"""Test we get the username data back."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.plugwise.config_flow.Smile",
) as smile_mock, patch(
"homeassistant.components.plugwise.async_setup_entry",
return_value=True,
) as mock_setup_entry:
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.gateway_id = "abcdefgh12345678"
smile_mock.return_value.smile_hostname = TEST_HOST
smile_mock.return_value.smile_name = "Adam"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
CONF_USERNAME: TEST_USERNAME2,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["data"] == {
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: DEFAULT_PORT,
CONF_USERNAME: TEST_USERNAME2,
PW_TYPE: API,
}
assert len(mock_setup_entry.mock_calls) == 1
result3 = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_ZEROCONF},
data=TEST_DISCOVERY,
)
assert result3["type"] == RESULT_TYPE_FORM
with patch(
"homeassistant.components.plugwise.config_flow.Smile",
) as smile_mock, patch(
"homeassistant.components.plugwise.async_setup_entry",
return_value=True,
) as mock_setup_entry:
smile_mock.return_value.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.gateway_id = "abcdefgh12345678"
smile_mock.return_value.smile_hostname = TEST_HOST
smile_mock.return_value.smile_name = "Adam"
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
user_input={CONF_PASSWORD: TEST_PASSWORD},
)
await hass.async_block_till_done()
assert result4["type"] == "abort"
assert result4["reason"] == "already_configured"
async def test_form_invalid_auth(hass, mock_smile):
@pytest.mark.parametrize(
"side_effect,reason",
[
(InvalidAuthentication, "invalid_auth"),
(ConnectionFailedError, "cannot_connect"),
(PlugwiseException, "cannot_connect"),
(RuntimeError, "unknown"),
],
)
async def test_flow_errors(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_smile_config_flow: MagicMock,
side_effect: Exception,
reason: str,
) -> None:
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET}
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("errors") == {}
assert result.get("step_id") == "user"
assert "flow_id" in result
mock_smile.connect.side_effect = InvalidAuthentication
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
mock_smile_config_flow.connect.side_effect = side_effect
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "invalid_auth"}
assert result2.get("type") == RESULT_TYPE_FORM
assert result2.get("errors") == {"base": reason}
assert result2.get("step_id") == "user"
assert len(mock_setup_entry.mock_calls) == 0
assert len(mock_smile_config_flow.connect.mock_calls) == 1
async def test_form_cannot_connect(hass, mock_smile):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET}
)
mock_smile.connect.side_effect = ConnectionFailedError
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
mock_smile_config_flow.connect.side_effect = None
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result3.get("title") == "Test Smile Name"
assert result3.get("data") == {
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: DEFAULT_PORT,
CONF_USERNAME: TEST_USERNAME,
PW_TYPE: API,
}
async def test_form_cannot_connect_port(hass, mock_smile):
"""Test we handle cannot connect to port error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET}
)
mock_smile.connect.side_effect = ConnectionFailedError
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_HOST: TEST_HOST,
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_other_problem(hass, mock_smile):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET}
)
mock_smile.connect.side_effect = TimeoutError
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "unknown"}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_smile_config_flow.connect.mock_calls) == 2