From 322e5c8d4b49f28bd664d5eb06b4e862621134bb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 30 Dec 2022 17:16:21 +0200 Subject: [PATCH 1/9] Initial Ruuvi Gateway integration --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/ruuvi_gateway/__init__.py | 45 ++++++++ .../components/ruuvi_gateway/bluetooth.py | 102 +++++++++++++++++ .../components/ruuvi_gateway/config_flow.py | 82 ++++++++++++++ .../components/ruuvi_gateway/const.py | 3 + .../components/ruuvi_gateway/coordinator.py | 49 +++++++++ .../components/ruuvi_gateway/manifest.json | 14 +++ .../components/ruuvi_gateway/models.py | 15 +++ .../components/ruuvi_gateway/schemata.py | 18 +++ .../components/ruuvi_gateway/strings.json | 20 ++++ .../ruuvi_gateway/translations/en.json | 20 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 4 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ruuvi_gateway/__init__.py | 1 + tests/components/ruuvi_gateway/consts.py | 11 ++ .../ruuvi_gateway/test_config_flow.py | 103 ++++++++++++++++++ tests/components/ruuvi_gateway/utils.py | 30 +++++ 22 files changed, 543 insertions(+) create mode 100644 homeassistant/components/ruuvi_gateway/__init__.py create mode 100644 homeassistant/components/ruuvi_gateway/bluetooth.py create mode 100644 homeassistant/components/ruuvi_gateway/config_flow.py create mode 100644 homeassistant/components/ruuvi_gateway/const.py create mode 100644 homeassistant/components/ruuvi_gateway/coordinator.py create mode 100644 homeassistant/components/ruuvi_gateway/manifest.json create mode 100644 homeassistant/components/ruuvi_gateway/models.py create mode 100644 homeassistant/components/ruuvi_gateway/schemata.py create mode 100644 homeassistant/components/ruuvi_gateway/strings.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/en.json create mode 100644 tests/components/ruuvi_gateway/__init__.py create mode 100644 tests/components/ruuvi_gateway/consts.py create mode 100644 tests/components/ruuvi_gateway/test_config_flow.py create mode 100644 tests/components/ruuvi_gateway/utils.py diff --git a/.strict-typing b/.strict-typing index 5f5a85034d8de..c76e8490ae606 100644 --- a/.strict-typing +++ b/.strict-typing @@ -247,6 +247,7 @@ homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.rpi_power.* homeassistant.components.rtsp_to_webrtc.* +homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvitag_ble.* homeassistant.components.samsungtv.* homeassistant.components.scene.* diff --git a/CODEOWNERS b/CODEOWNERS index 96187b24f9661..cadcdaa4870b5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -979,6 +979,8 @@ build.json @home-assistant/supervisor /tests/components/rtsp_to_webrtc/ @allenporter /homeassistant/components/ruckus_unleashed/ @gabe565 /tests/components/ruckus_unleashed/ @gabe565 +/homeassistant/components/ruuvi_gateway/ @akx +/tests/components/ruuvi_gateway/ @akx /homeassistant/components/ruuvitag_ble/ @akx /tests/components/ruuvitag_ble/ @akx /homeassistant/components/sabnzbd/ @shaiu diff --git a/homeassistant/components/ruuvi_gateway/__init__.py b/homeassistant/components/ruuvi_gateway/__init__.py new file mode 100644 index 0000000000000..8abc9e18d1792 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/__init__.py @@ -0,0 +1,45 @@ +"""The Ruuvi Gateway integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant + +from .bluetooth import async_connect_scanner +from .const import DOMAIN +from .coordinator import RuuviGatewayUpdateCoordinator +from .models import RuuviGatewayRuntimeData + +PLATFORMS: list[Platform] = [] + +_LOGGER = logging.getLogger(DOMAIN) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ruuvi Gateway from a config entry.""" + coordinator = RuuviGatewayUpdateCoordinator( + hass, + logger=_LOGGER, + name=entry.title, + update_interval=timedelta(seconds=10), + host=entry.data[CONF_HOST], + token=entry.data[CONF_TOKEN], + ) + scanner, unload_scanner = async_connect_scanner(hass, entry, coordinator) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RuuviGatewayRuntimeData( + update_coordinator=coordinator, + scanner=scanner, + ) + entry.async_on_unload(unload_scanner) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py new file mode 100644 index 0000000000000..247f99b9976a7 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -0,0 +1,102 @@ +"""Bluetooth support for Ruuvi Gateway.""" +from __future__ import annotations + +from collections.abc import Callable +import datetime +import logging + +from home_assistant_bluetooth import BluetoothServiceInfoBleak + +from homeassistant.components.bluetooth import ( + BaseHaRemoteScanner, + async_get_advertisement_callback, + async_register_scanner, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback + +from .coordinator import RuuviGatewayUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class RuuviGatewayScanner(BaseHaRemoteScanner): + """Scanner for Ruuvi Gateway.""" + + def __init__( + self, + hass: HomeAssistant, + scanner_id: str, + name: str, + new_info_callback: Callable[[BluetoothServiceInfoBleak], None], + *, + coordinator: RuuviGatewayUpdateCoordinator, + ) -> None: + """Initialize the scanner, using the given update coordinator as data source.""" + super().__init__( + hass, + scanner_id, + name, + new_info_callback, + connector=None, + connectable=False, + ) + self.coordinator = coordinator + + @callback + def _async_handle_new_data(self) -> None: + now = datetime.datetime.now() + for tag_data in self.coordinator.data: + if now - tag_data.datetime > datetime.timedelta(minutes=10): + # Don't process data that is older than 10 minutes + continue + anno = tag_data.parse_announcement() + self._async_on_advertisement( + address=tag_data.mac, + rssi=tag_data.rssi, + local_name=anno.local_name, + service_data=anno.service_data, + service_uuids=anno.service_uuids, + manufacturer_data=anno.manufacturer_data, + tx_power=anno.tx_power, + details={}, + ) + + @callback + def start_polling(self) -> CALLBACK_TYPE: + """Start polling; return a callback to stop polling.""" + return self.coordinator.async_add_listener(self._async_handle_new_data) + + +def async_connect_scanner( + hass: HomeAssistant, + entry: ConfigEntry, + coordinator: RuuviGatewayUpdateCoordinator, +) -> tuple[RuuviGatewayScanner, CALLBACK_TYPE]: + """Connect scanner and start polling.""" + assert entry.unique_id is not None + source = str(entry.unique_id) + _LOGGER.debug( + "%s [%s]: Connecting scanner", + entry.title, + source, + ) + scanner = RuuviGatewayScanner( + hass=hass, + scanner_id=source, + name=entry.title, + new_info_callback=async_get_advertisement_callback(hass), + coordinator=coordinator, + ) + unload_callbacks = [ + async_register_scanner(hass, scanner, connectable=False), + scanner.async_setup(), + scanner.start_polling(), + ] + + @callback + def _async_unload() -> None: + for unloader in unload_callbacks: + unloader() + + return (scanner, _async_unload) diff --git a/homeassistant/components/ruuvi_gateway/config_flow.py b/homeassistant/components/ruuvi_gateway/config_flow.py new file mode 100644 index 0000000000000..b2498b51a1b74 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/config_flow.py @@ -0,0 +1,82 @@ +"""Config flow for Ruuvi Gateway integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import aioruuvigateway.api as gw_api +from aioruuvigateway.excs import CannotConnect, InvalidAuth + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.httpx_client import get_async_client + +from . import DOMAIN +from .schemata import CONFIG_SCHEMA, get_config_schema_with_default_host + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ruuvi Gateway.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + super().__init__() + self.config_schema = CONFIG_SCHEMA + + async def _async_validate( + self, + user_input: dict[str, Any], + ) -> tuple[FlowResult | None, dict[str, str]]: + """Validate configuration (either discovered or user input).""" + errors: dict[str, str] = {} + + try: + async with get_async_client(self.hass) as client: + resp = await gw_api.get_gateway_history_data( + client, + host=user_input[CONF_HOST], + bearer_token=user_input[CONF_TOKEN], + ) + await self.async_set_unique_id(resp.gw_mac, raise_on_progress=False) + self._abort_if_unique_id_configured() + info = {"title": f"Ruuvi Gateway {resp.gw_mac_suffix}"} + return ( + self.async_create_entry(title=info["title"], data=user_input), + errors, + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return (None, errors) + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle requesting or validating user input.""" + if user_input is not None: + result, errors = await self._async_validate(user_input) + else: + result, errors = None, {} + if result is not None: + return result + return self.async_show_form( + step_id="user", + data_schema=self.config_schema, + errors=(errors or None), + ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Prepare configuration for a DHCP discovered Ruuvi Gateway.""" + self.config_schema = get_config_schema_with_default_host(host=discovery_info.ip) + return await self.async_step_user() diff --git a/homeassistant/components/ruuvi_gateway/const.py b/homeassistant/components/ruuvi_gateway/const.py new file mode 100644 index 0000000000000..56a9331ad6cc9 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/const.py @@ -0,0 +1,3 @@ +"""Constants for the Ruuvi Gateway integration.""" + +DOMAIN = "ruuvi_gateway" diff --git a/homeassistant/components/ruuvi_gateway/coordinator.py b/homeassistant/components/ruuvi_gateway/coordinator.py new file mode 100644 index 0000000000000..38bc3b0e20111 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/coordinator.py @@ -0,0 +1,49 @@ +"""Update coordinator for Ruuvi Gateway.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioruuvigateway.api import get_gateway_history_data +from aioruuvigateway.models import TagData + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +class RuuviGatewayUpdateCoordinator(DataUpdateCoordinator[list[TagData]]): + """Polls the gateway for data and returns a list of TagData objects that have changed since the last poll.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + update_interval: timedelta | None = None, + host: str, + token: str, + ) -> None: + """Initialize the coordinator using the given configuration (host, token).""" + super().__init__(hass, logger, name=name, update_interval=update_interval) + self.host = host + self.token = token + self.last_tag_datas: dict[str, TagData] = {} + + async def _async_update_data(self) -> list[TagData]: + changed_tag_datas: list[TagData] = [] + async with get_async_client(self.hass) as client: + data = await get_gateway_history_data( + client, + host=self.host, + bearer_token=self.token, + ) + for tag in data.tags: + if ( + tag.mac not in self.last_tag_datas + or self.last_tag_datas[tag.mac].data != tag.data + ): + changed_tag_datas.append(tag) + self.last_tag_datas[tag.mac] = tag + return changed_tag_datas diff --git a/homeassistant/components/ruuvi_gateway/manifest.json b/homeassistant/components/ruuvi_gateway/manifest.json new file mode 100644 index 0000000000000..1a42ebf6c1756 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "ruuvi_gateway", + "name": "Ruuvi Gateway", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ruuvi_gateway", + "codeowners": ["@akx"], + "requirements": ["aioruuvigateway==0.0.2"], + "iot_class": "local_polling", + "dhcp": [ + { + "hostname": "ruuvigateway*" + } + ] +} diff --git a/homeassistant/components/ruuvi_gateway/models.py b/homeassistant/components/ruuvi_gateway/models.py new file mode 100644 index 0000000000000..adb405f0bf808 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/models.py @@ -0,0 +1,15 @@ +"""Models for Ruuvi Gateway integration.""" +from __future__ import annotations + +import dataclasses + +from .bluetooth import RuuviGatewayScanner +from .coordinator import RuuviGatewayUpdateCoordinator + + +@dataclasses.dataclass(frozen=True) +class RuuviGatewayRuntimeData: + """Runtime data for Ruuvi Gateway integration.""" + + update_coordinator: RuuviGatewayUpdateCoordinator + scanner: RuuviGatewayScanner diff --git a/homeassistant/components/ruuvi_gateway/schemata.py b/homeassistant/components/ruuvi_gateway/schemata.py new file mode 100644 index 0000000000000..eec86cd129f22 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/schemata.py @@ -0,0 +1,18 @@ +"""Schemata for ruuvi_gateway.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_TOKEN + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_TOKEN): str, + } +) + + +def get_config_schema_with_default_host(host: str) -> vol.Schema: + """Return a config schema with a default host.""" + return CONFIG_SCHEMA.extend({vol.Required(CONF_HOST, default=host): str}) diff --git a/homeassistant/components/ruuvi_gateway/strings.json b/homeassistant/components/ruuvi_gateway/strings.json new file mode 100644 index 0000000000000..10b149c90692d --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host (IP address or DNS name)", + "token": "Bearer token (configured during gateway setup)" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} diff --git a/homeassistant/components/ruuvi_gateway/translations/en.json b/homeassistant/components/ruuvi_gateway/translations/en.json new file mode 100644 index 0000000000000..519623e32ce2d --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host (IP address or DNS name)", + "token": "Bearer token (configured during gateway setup)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d67b2a3aaac60..d87a28cec8c59 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -349,6 +349,7 @@ "rpi_power", "rtsp_to_webrtc", "ruckus_unleashed", + "ruuvi_gateway", "ruuvitag_ble", "sabnzbd", "samsungtv", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index f04fb56e32a3c..6ad3456b2544e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -400,6 +400,10 @@ "hostname": "roomba-*", "macaddress": "204EF6*", }, + { + "domain": "ruuvi_gateway", + "hostname": "ruuvigateway*", + }, { "domain": "samsungtv", "registered_devices": True, diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c266bc1b29bb4..0073581bf6377 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4569,6 +4569,12 @@ } } }, + "ruuvi_gateway": { + "name": "Ruuvi Gateway", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "ruuvitag_ble": { "name": "RuuviTag BLE", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 53cf4726a826a..26cb24c85b436 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2224,6 +2224,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ruuvi_gateway.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ruuvitag_ble.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 701597e059d65..a727f4015fbd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -260,6 +260,9 @@ aiorecollect==1.0.8 # homeassistant.components.ridwell aioridwell==2022.11.0 +# homeassistant.components.ruuvi_gateway +aioruuvigateway==0.0.2 + # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62a9b7f950767..87bf8c2c62c50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,6 +235,9 @@ aiorecollect==1.0.8 # homeassistant.components.ridwell aioridwell==2022.11.0 +# homeassistant.components.ruuvi_gateway +aioruuvigateway==0.0.2 + # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/tests/components/ruuvi_gateway/__init__.py b/tests/components/ruuvi_gateway/__init__.py new file mode 100644 index 0000000000000..219eb09f77466 --- /dev/null +++ b/tests/components/ruuvi_gateway/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ruuvi Gateway integration.""" diff --git a/tests/components/ruuvi_gateway/consts.py b/tests/components/ruuvi_gateway/consts.py new file mode 100644 index 0000000000000..2653349e63419 --- /dev/null +++ b/tests/components/ruuvi_gateway/consts.py @@ -0,0 +1,11 @@ +"""Constants for ruuvi_gateway tests.""" +from __future__ import annotations + +ASYNC_SETUP_ENTRY = "homeassistant.components.ruuvi_gateway.async_setup_entry" +GET_GATEWAY_HISTORY_DATA = "aioruuvigateway.api.get_gateway_history_data" +EXPECTED_TITLE = "Ruuvi Gateway EE:FF" +BASE_DATA = { + "host": "1.1.1.1", + "token": "toktok", +} +GATEWAY_MAC = "AA:BB:CC:DD:EE:FF" diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py new file mode 100644 index 0000000000000..a3293c9fc13d1 --- /dev/null +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -0,0 +1,103 @@ +"""Test the Ruuvi Gateway config flow.""" +from unittest.mock import patch + +from aioruuvigateway.excs import CannotConnect, InvalidAuth +import pytest + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.components.ruuvi_gateway.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .consts import BASE_DATA, EXPECTED_TITLE, GATEWAY_MAC, GET_GATEWAY_HISTORY_DATA +from .utils import patch_gateway_ok, patch_setup_entry_ok + + +@pytest.mark.parametrize("method", ["user", "dhcp"]) +async def test_ok_setup(hass: HomeAssistant, method: str) -> None: + """Test we get the form.""" + if method == "user": + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + data = entry = BASE_DATA + elif method == "dhcp": + dhcp_ip = "1.2.3.4" + data = entry = {**BASE_DATA, "host": dhcp_ip} + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=dhcp.DhcpServiceInfo( + hostname="RuuviGateway1234", + ip=dhcp_ip, + macaddress="12:34:56:78:90:ab", + ), + context={"source": config_entries.SOURCE_DHCP}, + ) + else: + raise NotImplementedError("...") + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER + assert result["errors"] is None + + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + entry, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == EXPECTED_TITLE + assert result2["data"] == data + assert result2["context"]["unique_id"] == GATEWAY_MAC + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=InvalidAuth): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + BASE_DATA, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=CannotConnect): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + BASE_DATA, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unexpected(hass: HomeAssistant) -> None: + """Test we handle unexpected errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=MemoryError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + BASE_DATA, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ruuvi_gateway/utils.py b/tests/components/ruuvi_gateway/utils.py new file mode 100644 index 0000000000000..d3181ca8f5f9a --- /dev/null +++ b/tests/components/ruuvi_gateway/utils.py @@ -0,0 +1,30 @@ +"""Utilities for ruuvi_gateway tests.""" +from __future__ import annotations + +import time +from unittest.mock import _patch, patch + +from aioruuvigateway.models import HistoryResponse + +from tests.components.ruuvi_gateway.consts import ( + ASYNC_SETUP_ENTRY, + GATEWAY_MAC, + GET_GATEWAY_HISTORY_DATA, +) + + +def patch_gateway_ok() -> _patch: + """Patch gateway function to return valid data.""" + return patch( + GET_GATEWAY_HISTORY_DATA, + return_value=HistoryResponse( + timestamp=int(time.time()), + gw_mac=GATEWAY_MAC, + tags=[], + ), + ) + + +def patch_setup_entry_ok() -> _patch: + """Patch setup entry to return True.""" + return patch(ASYNC_SETUP_ENTRY, return_value=True) From f3b3a94007ca82f7486fba84ea370726e960296c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sun, 1 Jan 2023 18:48:19 +0200 Subject: [PATCH 2/9] Changes from initial review --- homeassistant/components/ruuvi_gateway/__init__.py | 11 ++++------- homeassistant/components/ruuvi_gateway/bluetooth.py | 3 ++- homeassistant/components/ruuvi_gateway/const.py | 9 +++++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ruuvi_gateway/__init__.py b/homeassistant/components/ruuvi_gateway/__init__.py index 8abc9e18d1792..59d37abbf7be5 100644 --- a/homeassistant/components/ruuvi_gateway/__init__.py +++ b/homeassistant/components/ruuvi_gateway/__init__.py @@ -1,20 +1,17 @@ """The Ruuvi Gateway integration.""" from __future__ import annotations -from datetime import timedelta import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform +from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant from .bluetooth import async_connect_scanner -from .const import DOMAIN +from .const import DOMAIN, SCAN_INTERVAL from .coordinator import RuuviGatewayUpdateCoordinator from .models import RuuviGatewayRuntimeData -PLATFORMS: list[Platform] = [] - _LOGGER = logging.getLogger(DOMAIN) @@ -24,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, logger=_LOGGER, name=entry.title, - update_interval=timedelta(seconds=10), + update_interval=SCAN_INTERVAL, host=entry.data[CONF_HOST], token=entry.data[CONF_TOKEN], ) @@ -39,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + if unload_ok := await hass.config_entries.async_unload_platforms(entry, []): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py index 247f99b9976a7..f5748b8b4e996 100644 --- a/homeassistant/components/ruuvi_gateway/bluetooth.py +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from .const import OLD_ADVERTISEMENT_CUTOFF from .coordinator import RuuviGatewayUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -47,7 +48,7 @@ def __init__( def _async_handle_new_data(self) -> None: now = datetime.datetime.now() for tag_data in self.coordinator.data: - if now - tag_data.datetime > datetime.timedelta(minutes=10): + if now - tag_data.datetime > OLD_ADVERTISEMENT_CUTOFF: # Don't process data that is older than 10 minutes continue anno = tag_data.parse_announcement() diff --git a/homeassistant/components/ruuvi_gateway/const.py b/homeassistant/components/ruuvi_gateway/const.py index 56a9331ad6cc9..609bad9a22661 100644 --- a/homeassistant/components/ruuvi_gateway/const.py +++ b/homeassistant/components/ruuvi_gateway/const.py @@ -1,3 +1,12 @@ """Constants for the Ruuvi Gateway integration.""" +from datetime import timedelta + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) DOMAIN = "ruuvi_gateway" +SCAN_INTERVAL = timedelta(seconds=5) +OLD_ADVERTISEMENT_CUTOFF = timedelta( + seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS +) From 0c92c20cfa04c260436a72ae52cba532690f2bf3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 2 Jan 2023 09:42:02 +0200 Subject: [PATCH 3/9] Update homeassistant/components/ruuvi_gateway/config_flow.py Co-authored-by: J. Nick Koston --- homeassistant/components/ruuvi_gateway/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ruuvi_gateway/config_flow.py b/homeassistant/components/ruuvi_gateway/config_flow.py index b2498b51a1b74..e164ea1df04d3 100644 --- a/homeassistant/components/ruuvi_gateway/config_flow.py +++ b/homeassistant/components/ruuvi_gateway/config_flow.py @@ -44,7 +44,9 @@ async def _async_validate( bearer_token=user_input[CONF_TOKEN], ) await self.async_set_unique_id(resp.gw_mac, raise_on_progress=False) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured( + updates={CONF_HOST: user_input[CONF_HOST]} + ) info = {"title": f"Ruuvi Gateway {resp.gw_mac_suffix}"} return ( self.async_create_entry(title=info["title"], data=user_input), From e61702133d7621f16d16ecf068f52eff91d9088f Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 2 Jan 2023 12:10:49 +0200 Subject: [PATCH 4/9] Coverage-ignore currently untested modules --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 42b43b93d4332..871591a0ceb6b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1082,6 +1082,9 @@ omit = homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rtorrent/sensor.py + homeassistant/components/ruuvi_gateway/__init__.py + homeassistant/components/ruuvi_gateway/bluetooth.py + homeassistant/components/ruuvi_gateway/coordinator.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/__init__.py From 5483bcfb1afb6ba1dc9ce6eb3b3955d2f08bdd2e Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 2 Jan 2023 12:14:13 +0200 Subject: [PATCH 5/9] Move test parametrization --- .../ruuvi_gateway/test_config_flow.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index a3293c9fc13d1..44639206cdc8b 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -13,30 +13,37 @@ from .consts import BASE_DATA, EXPECTED_TITLE, GATEWAY_MAC, GET_GATEWAY_HISTORY_DATA from .utils import patch_gateway_ok, patch_setup_entry_ok +DHCP_IP = "1.2.3.4" +DHCP_DATA = {**BASE_DATA, "host": DHCP_IP} -@pytest.mark.parametrize("method", ["user", "dhcp"]) -async def test_ok_setup(hass: HomeAssistant, method: str) -> None: - """Test we get the form.""" - if method == "user": - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - ) - data = entry = BASE_DATA - elif method == "dhcp": - dhcp_ip = "1.2.3.4" - data = entry = {**BASE_DATA, "host": dhcp_ip} - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=dhcp.DhcpServiceInfo( + +@pytest.mark.parametrize( + "init_data, init_context, entry", + [ + ( + None, + {"source": config_entries.SOURCE_USER}, + BASE_DATA, + ), + ( + dhcp.DhcpServiceInfo( hostname="RuuviGateway1234", - ip=dhcp_ip, + ip=DHCP_IP, macaddress="12:34:56:78:90:ab", ), - context={"source": config_entries.SOURCE_DHCP}, - ) - else: - raise NotImplementedError("...") + {"source": config_entries.SOURCE_DHCP}, + DHCP_DATA, + ), + ], + ids=["user", "dhcp"], +) +async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=init_data, + context=init_context, + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER assert result["errors"] is None @@ -50,7 +57,7 @@ async def test_ok_setup(hass: HomeAssistant, method: str) -> None: assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == EXPECTED_TITLE - assert result2["data"] == data + assert result2["data"] == entry assert result2["context"]["unique_id"] == GATEWAY_MAC assert len(mock_setup_entry.mock_calls) == 1 From b663261d2cd0b2f1026e1b11b986daca97c910e3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 2 Jan 2023 12:17:07 +0200 Subject: [PATCH 6/9] Embetter names in test --- .../ruuvi_gateway/test_config_flow.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index 44639206cdc8b..ad0586bae6216 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -39,72 +39,72 @@ ) async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> None: """Test we get the form.""" - result = await hass.config_entries.flow.async_init( + init_result = await hass.config_entries.flow.async_init( DOMAIN, data=init_data, context=init_context, ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == config_entries.SOURCE_USER - assert result["errors"] is None + assert init_result["type"] == FlowResultType.FORM + assert init_result["step_id"] == config_entries.SOURCE_USER + assert init_result["errors"] is None with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], entry, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == EXPECTED_TITLE - assert result2["data"] == entry - assert result2["context"]["unique_id"] == GATEWAY_MAC + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == entry + assert config_result["context"]["unique_id"] == GATEWAY_MAC assert len(mock_setup_entry.mock_calls) == 1 async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( + init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch(GET_GATEWAY_HISTORY_DATA, side_effect=InvalidAuth): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], BASE_DATA, ) - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "invalid_auth"} async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( + init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch(GET_GATEWAY_HISTORY_DATA, side_effect=CannotConnect): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], BASE_DATA, ) - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "cannot_connect"} async def test_form_unexpected(hass: HomeAssistant) -> None: """Test we handle unexpected errors.""" - result = await hass.config_entries.flow.async_init( + init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch(GET_GATEWAY_HISTORY_DATA, side_effect=MemoryError): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], BASE_DATA, ) - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "unknown"} From be17703b0c1962480241ed07d0d3c4881b906a18 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 3 Jan 2023 21:37:42 +0200 Subject: [PATCH 7/9] Apply suggestions from code review Co-authored-by: J. Nick Koston --- homeassistant/components/ruuvi_gateway/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ruuvi_gateway/config_flow.py b/homeassistant/components/ruuvi_gateway/config_flow.py index e164ea1df04d3..fd052268578aa 100644 --- a/homeassistant/components/ruuvi_gateway/config_flow.py +++ b/homeassistant/components/ruuvi_gateway/config_flow.py @@ -43,7 +43,7 @@ async def _async_validate( host=user_input[CONF_HOST], bearer_token=user_input[CONF_TOKEN], ) - await self.async_set_unique_id(resp.gw_mac, raise_on_progress=False) + await self.async_set_unique_id(format_mac(resp.gw_mac), raise_on_progress=False) self._abort_if_unique_id_configured( updates={CONF_HOST: user_input[CONF_HOST]} ) @@ -80,5 +80,7 @@ async def async_step_user( async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Prepare configuration for a DHCP discovered Ruuvi Gateway.""" + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) self.config_schema = get_config_schema_with_default_host(host=discovery_info.ip) return await self.async_step_user() From a79387420d218841cdb5cdeba2923c262b0db0c6 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 3 Jan 2023 21:37:02 +0200 Subject: [PATCH 8/9] Add flow finalization to all tests (h/t bdraco) --- .../components/ruuvi_gateway/config_flow.py | 5 +- tests/components/ruuvi_gateway/consts.py | 1 + .../ruuvi_gateway/test_config_flow.py | 51 ++++++++++++++----- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ruuvi_gateway/config_flow.py b/homeassistant/components/ruuvi_gateway/config_flow.py index fd052268578aa..178c55a53e4bc 100644 --- a/homeassistant/components/ruuvi_gateway/config_flow.py +++ b/homeassistant/components/ruuvi_gateway/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.httpx_client import get_async_client from . import DOMAIN @@ -43,7 +44,9 @@ async def _async_validate( host=user_input[CONF_HOST], bearer_token=user_input[CONF_TOKEN], ) - await self.async_set_unique_id(format_mac(resp.gw_mac), raise_on_progress=False) + await self.async_set_unique_id( + format_mac(resp.gw_mac), raise_on_progress=False + ) self._abort_if_unique_id_configured( updates={CONF_HOST: user_input[CONF_HOST]} ) diff --git a/tests/components/ruuvi_gateway/consts.py b/tests/components/ruuvi_gateway/consts.py index 2653349e63419..bd544fb209894 100644 --- a/tests/components/ruuvi_gateway/consts.py +++ b/tests/components/ruuvi_gateway/consts.py @@ -9,3 +9,4 @@ "token": "toktok", } GATEWAY_MAC = "AA:BB:CC:DD:EE:FF" +GATEWAY_MAC_LOWER = GATEWAY_MAC.lower() diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index ad0586bae6216..a4b6300dd5c5b 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -8,9 +8,14 @@ from homeassistant.components import dhcp from homeassistant.components.ruuvi_gateway.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import FlowResult, FlowResultType -from .consts import BASE_DATA, EXPECTED_TITLE, GATEWAY_MAC, GET_GATEWAY_HISTORY_DATA +from .consts import ( + BASE_DATA, + EXPECTED_TITLE, + GATEWAY_MAC_LOWER, + GET_GATEWAY_HISTORY_DATA, +) from .utils import patch_gateway_ok, patch_setup_entry_ok DHCP_IP = "1.2.3.4" @@ -48,18 +53,8 @@ async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> assert init_result["step_id"] == config_entries.SOURCE_USER assert init_result["errors"] is None - with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: - config_result = await hass.config_entries.flow.async_configure( - init_result["flow_id"], - entry, - ) - await hass.async_block_till_done() - - assert config_result["type"] == FlowResultType.CREATE_ENTRY - assert config_result["title"] == EXPECTED_TITLE - assert config_result["data"] == entry - assert config_result["context"]["unique_id"] == GATEWAY_MAC - assert len(mock_setup_entry.mock_calls) == 1 + # Check that setup is okay + await assert_finalize_setup(hass, entry, init_result) async def test_form_invalid_auth(hass: HomeAssistant) -> None: @@ -77,6 +72,9 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert config_result["type"] == FlowResultType.FORM assert config_result["errors"] == {"base": "invalid_auth"} + # Check that we still can finalize setup + await assert_finalize_setup(hass, BASE_DATA, init_result) + async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" @@ -93,6 +91,9 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert config_result["type"] == FlowResultType.FORM assert config_result["errors"] == {"base": "cannot_connect"} + # Check that we still can finalize setup + await assert_finalize_setup(hass, BASE_DATA, init_result) + async def test_form_unexpected(hass: HomeAssistant) -> None: """Test we handle unexpected errors.""" @@ -108,3 +109,25 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: assert config_result["type"] == FlowResultType.FORM assert config_result["errors"] == {"base": "unknown"} + + # Check that we still can finalize setup + await assert_finalize_setup(hass, BASE_DATA, init_result) + + +async def assert_finalize_setup( + hass: HomeAssistant, + entry: dict, + init_result: FlowResult, +) -> None: + """Help multiple tests check that we can finalize setup.""" + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + entry, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == entry + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 From 26240002da59a803c118a04a75b1cd90c3e32929 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 3 Jan 2023 21:51:32 +0200 Subject: [PATCH 9/9] Inline assert_finalize_setup test helper --- .../ruuvi_gateway/test_config_flow.py | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index a4b6300dd5c5b..4f7e1ae116e37 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import dhcp from homeassistant.components.ruuvi_gateway.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult, FlowResultType +from homeassistant.data_entry_flow import FlowResultType from .consts import ( BASE_DATA, @@ -53,8 +53,18 @@ async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> assert init_result["step_id"] == config_entries.SOURCE_USER assert init_result["errors"] is None - # Check that setup is okay - await assert_finalize_setup(hass, entry, init_result) + # Check that we can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + entry, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == entry + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 async def test_form_invalid_auth(hass: HomeAssistant) -> None: @@ -73,7 +83,17 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert config_result["errors"] == {"base": "invalid_auth"} # Check that we still can finalize setup - await assert_finalize_setup(hass, BASE_DATA, init_result) + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -92,7 +112,17 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert config_result["errors"] == {"base": "cannot_connect"} # Check that we still can finalize setup - await assert_finalize_setup(hass, BASE_DATA, init_result) + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 async def test_form_unexpected(hass: HomeAssistant) -> None: @@ -111,23 +141,14 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: assert config_result["errors"] == {"base": "unknown"} # Check that we still can finalize setup - await assert_finalize_setup(hass, BASE_DATA, init_result) - - -async def assert_finalize_setup( - hass: HomeAssistant, - entry: dict, - init_result: FlowResult, -) -> None: - """Help multiple tests check that we can finalize setup.""" with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: config_result = await hass.config_entries.flow.async_configure( init_result["flow_id"], - entry, + BASE_DATA, ) await hass.async_block_till_done() assert config_result["type"] == FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE - assert config_result["data"] == entry + assert config_result["data"] == BASE_DATA assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER assert len(mock_setup_entry.mock_calls) == 1