From d828263ee3e3b813a9d864cf1fc7081360c3a237 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 10 Mar 2023 02:15:22 +0100 Subject: [PATCH 01/16] Add device class to ZHA Xiaomi plug "consumer connected" sensor (#89476) Add device class to ZHA Xiaomi plug "consumer connected" --- homeassistant/components/zha/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index dc5a5eebbaa..b6a0af8e459 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -213,3 +213,4 @@ class XiaomiPlugConsumerConnected(BinarySensor, id_suffix="consumer_connected"): SENSOR_ATTR = "consumer_connected" _attr_name: str = "Consumer connected" + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PLUG From 9e1ba8534abfc16c46706ba31c2b1bd2d0dd1c87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Mar 2023 16:03:41 -1000 Subject: [PATCH 02/16] Fix data migration never finishing when database has invalid datetimes (#89474) * Fix data migration never finishing when database has invalid datetimes If there were impossible datetime values in the database (likely from a manual sqlite to MySQL conversion) the conversion would never complete * Update homeassistant/components/recorder/migration.py --- homeassistant/components/recorder/migration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e0f1163491e..a5ff110e57e 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1106,7 +1106,7 @@ def _migrate_columns_to_timestamp( result = session.connection().execute( text( "UPDATE events set time_fired_ts=" - "IF(time_fired is NULL,0," + "IF(time_fired is NULL or UNIX_TIMESTAMP(time_fired) is NULL,0," "UNIX_TIMESTAMP(time_fired)" ") " "where time_fired_ts is NULL " @@ -1119,7 +1119,7 @@ def _migrate_columns_to_timestamp( result = session.connection().execute( text( "UPDATE states set last_updated_ts=" - "IF(last_updated is NULL,0," + "IF(last_updated is NULL or UNIX_TIMESTAMP(last_updated) is NULL,0," "UNIX_TIMESTAMP(last_updated) " "), " "last_changed_ts=" @@ -1195,7 +1195,7 @@ def _migrate_statistics_columns_to_timestamp( result = session.connection().execute( text( f"UPDATE {table} set start_ts=" - "IF(start is NULL,0," + "IF(start is NULL or UNIX_TIMESTAMP(start) is NULL,0," "UNIX_TIMESTAMP(start) " "), " "created_ts=" From fde205c158b704683650cec1d5832ca0858bbd21 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Mar 2023 04:32:32 +0100 Subject: [PATCH 03/16] Add unconfigured flag to thread discovery data (#89230) Co-authored-by: Paulus Schoutsen --- homeassistant/components/thread/discovery.py | 27 ++++- tests/components/thread/__init__.py | 106 ++++++++++++++++++ tests/components/thread/test_discovery.py | 69 +++++++++++- tests/components/thread/test_websocket_api.py | 10 +- 4 files changed, 197 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index 5a2ee54c5bb..b2373ff9825 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -6,6 +6,7 @@ import dataclasses import logging from typing import cast +from python_otbr_api.mdns import StateBitmap from zeroconf import BadTypeInNameException, DNSPointer, ServiceListener, Zeroconf from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf @@ -29,14 +30,15 @@ TYPE_PTR = 12 class ThreadRouterDiscoveryData: """Thread router discovery data.""" + addresses: list[str] | None brand: str | None extended_pan_id: str | None model_name: str | None network_name: str | None server: str | None - vendor_name: str | None - addresses: list[str] | None thread_version: str | None + unconfigured: bool | None + vendor_name: str | None def async_discovery_data_from_service( @@ -59,15 +61,30 @@ def async_discovery_data_from_service( server = service.server vendor_name = try_decode(service.properties.get(b"vn")) thread_version = try_decode(service.properties.get(b"tv")) + unconfigured = None + brand = KNOWN_BRANDS.get(vendor_name) + if brand == "homeassistant": + # Attempt to detect incomplete configuration + if (state_bitmap_b := service.properties.get(b"sb")) is not None: + try: + state_bitmap = StateBitmap.from_bytes(state_bitmap_b) + if not state_bitmap.is_active: + unconfigured = True + except ValueError: + _LOGGER.debug("Failed to decode state bitmap in service %s", service) + if service.properties.get(b"at") is None: + unconfigured = True + return ThreadRouterDiscoveryData( - brand=KNOWN_BRANDS.get(vendor_name), + addresses=service.parsed_addresses(), + brand=brand, extended_pan_id=ext_pan_id.hex() if ext_pan_id is not None else None, model_name=model_name, network_name=network_name, server=server, - vendor_name=vendor_name, - addresses=service.parsed_addresses(), thread_version=thread_version, + unconfigured=unconfigured, + vendor_name=vendor_name, ) diff --git a/tests/components/thread/__init__.py b/tests/components/thread/__init__.py index c8e4453da1f..fd3cc3d9d85 100644 --- a/tests/components/thread/__init__.py +++ b/tests/components/thread/__init__.py @@ -185,3 +185,109 @@ ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA = { }, "interface_index": None, } + + +ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP = { + "type_": "_meshcop._udp.local.", + "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", + "addresses": [b"\xc0\xa8\x00s"], + "port": 49153, + "weight": 0, + "priority": 0, + "server": "core-silabs-multiprotocol.local.", + "properties": { + b"rv": b"1", + b"vn": b"HomeAssistant", + b"mn": b"OpenThreadBorderRouter", + b"nn": b"OpenThread HC", + b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5", + b"tv": b"1.3.0", + b"xa": b"\xae\xeb/YKW\x0b\xbf", + b"sb": b"\x00\x00\x01\xb1", + b"pt": b"\x8f\x06Q~", + b"sq": b"3", + b"bb": b"\xf0\xbf", + b"dn": b"DefaultDomain", + }, + "interface_index": None, +} + + +ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP = { + "type_": "_meshcop._udp.local.", + "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", + "addresses": [b"\xc0\xa8\x00s"], + "port": 49153, + "weight": 0, + "priority": 0, + "server": "core-silabs-multiprotocol.local.", + "properties": { + b"rv": b"1", + b"vn": b"HomeAssistant", + b"mn": b"OpenThreadBorderRouter", + b"nn": b"OpenThread HC", + b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5", + b"tv": b"1.3.0", + b"xa": b"\xae\xeb/YKW\x0b\xbf", + b"at": b"\x00\x00\x00\x00\x00\x01\x00\x00", + b"pt": b"\x8f\x06Q~", + b"sq": b"3", + b"bb": b"\xf0\xbf", + b"dn": b"DefaultDomain", + }, + "interface_index": None, +} + + +ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP = { + "type_": "_meshcop._udp.local.", + "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", + "addresses": [b"\xc0\xa8\x00s"], + "port": 49153, + "weight": 0, + "priority": 0, + "server": "core-silabs-multiprotocol.local.", + "properties": { + b"rv": b"1", + b"vn": b"HomeAssistant", + b"mn": b"OpenThreadBorderRouter", + b"nn": b"OpenThread HC", + b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5", + b"tv": b"1.3.0", + b"xa": b"\xae\xeb/YKW\x0b\xbf", + b"sb": b"\xff\x00\x01\xb1", + b"at": b"\x00\x00\x00\x00\x00\x01\x00\x00", + b"pt": b"\x8f\x06Q~", + b"sq": b"3", + b"bb": b"\xf0\xbf", + b"dn": b"DefaultDomain", + }, + "interface_index": None, +} + + +ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE = { + "type_": "_meshcop._udp.local.", + "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", + "addresses": [b"\xc0\xa8\x00s"], + "port": 49153, + "weight": 0, + "priority": 0, + "server": "core-silabs-multiprotocol.local.", + "properties": { + b"rv": b"1", + b"vn": b"HomeAssistant", + b"mn": b"OpenThreadBorderRouter", + b"nn": b"OpenThread HC", + b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5", + b"tv": b"1.3.0", + b"xa": b"\xae\xeb/YKW\x0b\xbf", + b"sb": b"\x00\x00\x01\x31", + b"at": b"\x00\x00\x00\x00\x00\x01\x00\x00", + b"pt": b"\x8f\x06Q~", + b"sq": b"3", + b"bb": b"\xf0\xbf", + b"dn": b"DefaultDomain", + }, + "interface_index": None, +} diff --git a/tests/components/thread/test_discovery.py b/tests/components/thread/test_discovery.py index fc19b3f10ac..e832f18c4e6 100644 --- a/tests/components/thread/test_discovery.py +++ b/tests/components/thread/test_discovery.py @@ -14,8 +14,12 @@ from . import ( ROUTER_DISCOVERY_GOOGLE_1, ROUTER_DISCOVERY_HASS, ROUTER_DISCOVERY_HASS_BAD_DATA, + ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP, ROUTER_DISCOVERY_HASS_MISSING_DATA, ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA, + ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP, + ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP, + ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE, ) @@ -67,14 +71,15 @@ async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) assert discovered[-1] == ( "aeeb2f594b570bbf", discovery.ThreadRouterDiscoveryData( + addresses=["192.168.0.115"], brand="homeassistant", extended_pan_id="e60fc7c186212ce5", model_name="OpenThreadBorderRouter", network_name="OpenThread HC", server="core-silabs-multiprotocol.local.", - vendor_name="HomeAssistant", thread_version="1.3.0", - addresses=["192.168.0.115"], + unconfigured=None, + vendor_name="HomeAssistant", ), ) @@ -91,14 +96,15 @@ async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) assert discovered[-1] == ( "f6a99b425a67abed", discovery.ThreadRouterDiscoveryData( + addresses=["192.168.0.124"], brand="google", extended_pan_id="9e75e256f61409a3", model_name="Google Nest Hub", network_name="NEST-PAN-E1AF", server="2d99f293-cd8e-2770-8dd2-6675de9fa000.local.", - vendor_name="Google Inc.", thread_version="1.3.0", - addresses=["192.168.0.124"], + unconfigured=None, + vendor_name="Google Inc.", ), ) @@ -130,6 +136,56 @@ async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) mock_async_zeroconf.async_remove_service_listener.assert_called_once_with(listener) +@pytest.mark.parametrize( + ("data", "unconfigured"), + [ + (ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP, True), + (ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP, None), + (ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP, None), + (ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE, True), + ], +) +async def test_discover_routers_unconfigured( + hass: HomeAssistant, mock_async_zeroconf: None, data, unconfigured +) -> None: + """Test discovering thread routers with bad or missing vendor mDNS data.""" + mock_async_zeroconf.async_add_service_listener = AsyncMock() + mock_async_zeroconf.async_remove_service_listener = AsyncMock() + mock_async_zeroconf.async_get_service_info = AsyncMock() + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + # Start Thread router discovery + router_discovered_removed = Mock() + thread_disovery = discovery.ThreadRouterDiscovery( + hass, router_discovered_removed, router_discovered_removed + ) + await thread_disovery.async_start() + listener: discovery.ThreadRouterDiscovery.ThreadServiceListener = ( + mock_async_zeroconf.async_add_service_listener.mock_calls[0][1][1] + ) + + # Discover a service with bad or missing data + mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo(**data) + listener.add_service(None, data["type_"], data["name"]) + await hass.async_block_till_done() + router_discovered_removed.assert_called_once_with( + "aeeb2f594b570bbf", + discovery.ThreadRouterDiscoveryData( + addresses=["192.168.0.115"], + brand="homeassistant", + extended_pan_id="e60fc7c186212ce5", + model_name="OpenThreadBorderRouter", + network_name="OpenThread HC", + server="core-silabs-multiprotocol.local.", + thread_version="1.3.0", + unconfigured=unconfigured, + vendor_name="HomeAssistant", + ), + ) + + @pytest.mark.parametrize( "data", (ROUTER_DISCOVERY_HASS_BAD_DATA, ROUTER_DISCOVERY_HASS_MISSING_DATA) ) @@ -161,14 +217,15 @@ async def test_discover_routers_bad_data( router_discovered_removed.assert_called_once_with( "aeeb2f594b570bbf", discovery.ThreadRouterDiscoveryData( + addresses=["192.168.0.115"], brand=None, extended_pan_id="e60fc7c186212ce5", model_name="OpenThreadBorderRouter", network_name="OpenThread HC", server="core-silabs-multiprotocol.local.", - vendor_name=None, thread_version="1.3.0", - addresses=["192.168.0.115"], + unconfigured=None, + vendor_name=None, ), ) diff --git a/tests/components/thread/test_websocket_api.py b/tests/components/thread/test_websocket_api.py index 2ebeef92c52..0f3a2ff7654 100644 --- a/tests/components/thread/test_websocket_api.py +++ b/tests/components/thread/test_websocket_api.py @@ -234,14 +234,15 @@ async def test_discover_routers( assert msg == { "event": { "data": { + "addresses": ["192.168.0.115"], "brand": "homeassistant", "extended_pan_id": "e60fc7c186212ce5", "model_name": "OpenThreadBorderRouter", "network_name": "OpenThread HC", "server": "core-silabs-multiprotocol.local.", - "vendor_name": "HomeAssistant", - "addresses": ["192.168.0.115"], "thread_version": "1.3.0", + "unconfigured": None, + "vendor_name": "HomeAssistant", }, "key": "aeeb2f594b570bbf", "type": "router_discovered", @@ -261,14 +262,15 @@ async def test_discover_routers( assert msg == { "event": { "data": { + "addresses": ["192.168.0.124"], "brand": "google", "extended_pan_id": "9e75e256f61409a3", "model_name": "Google Nest Hub", "network_name": "NEST-PAN-E1AF", "server": "2d99f293-cd8e-2770-8dd2-6675de9fa000.local.", - "vendor_name": "Google Inc.", "thread_version": "1.3.0", - "addresses": ["192.168.0.124"], + "unconfigured": None, + "vendor_name": "Google Inc.", }, "key": "f6a99b425a67abed", "type": "router_discovered", From b8bda93d87f35d7862d0014f5a0e876cb2556a42 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Fri, 10 Mar 2023 10:26:03 +0100 Subject: [PATCH 04/16] Add config flow to frontier_silicon (#64365) * Add config_flow to frontier_silicon * Add missing translation file * Delay unique_id validation until radio_id can be determined * Fix tests * Improve tests * Use FlowResultType * Bump afsapi to 0.2.6 * Fix requirements_test_all.txt * Stash ssdp, reauth and unignore flows for now * Re-introduce SSDP flow * hassfest changes * Address review comments * Small style update * Fix tests * Update integrations.json * fix order in manifest.json * fix black errors * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Address review comments * fix black errors * Use async_setup_platform instead of async_setup * Address review comments on tests * parameterize tests * Remove discovery component changes from this PR * Address review comments * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Add extra asserts to tests * Restructure _async_step_device_config_if_needed * Add return statement * Update homeassistant/components/frontier_silicon/media_player.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 3 +- CODEOWNERS | 1 + .../components/frontier_silicon/__init__.py | 46 ++- .../frontier_silicon/config_flow.py | 178 ++++++++++++ .../components/frontier_silicon/const.py | 3 + .../components/frontier_silicon/manifest.json | 1 + .../frontier_silicon/media_player.py | 57 ++-- .../components/frontier_silicon/strings.json | 35 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/frontier_silicon/__init__.py | 1 + tests/components/frontier_silicon/conftest.py | 59 ++++ .../frontier_silicon/test_config_flow.py | 266 ++++++++++++++++++ 14 files changed, 636 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/frontier_silicon/config_flow.py create mode 100644 homeassistant/components/frontier_silicon/strings.json create mode 100644 tests/components/frontier_silicon/__init__.py create mode 100644 tests/components/frontier_silicon/conftest.py create mode 100644 tests/components/frontier_silicon/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index fa6ae5ba0d2..a533343bf06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -395,7 +395,8 @@ omit = homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/sensor.py - homeassistant/components/frontier_silicon/const.py + homeassistant/components/frontier_silicon/__init__.py + homeassistant/components/frontier_silicon/browse_media.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 9020d229fe9..f95d89fec47 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -401,6 +401,7 @@ build.json @home-assistant/supervisor /homeassistant/components/frontend/ @home-assistant/frontend /tests/components/frontend/ @home-assistant/frontend /homeassistant/components/frontier_silicon/ @wlcrs +/tests/components/frontier_silicon/ @wlcrs /homeassistant/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood /homeassistant/components/garages_amsterdam/ @klaasnicolaas diff --git a/homeassistant/components/frontier_silicon/__init__.py b/homeassistant/components/frontier_silicon/__init__.py index ddd74ca8efe..4a884063f83 100644 --- a/homeassistant/components/frontier_silicon/__init__.py +++ b/homeassistant/components/frontier_silicon/__init__.py @@ -1 +1,45 @@ -"""The frontier_silicon component.""" +"""The Frontier Silicon integration.""" +from __future__ import annotations + +import logging + +from afsapi import AFSAPI, ConnectionError as FSConnectionError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady + +from .const import CONF_PIN, CONF_WEBFSAPI_URL, DOMAIN + +PLATFORMS = [Platform.MEDIA_PLAYER] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Frontier Silicon from a config entry.""" + + webfsapi_url = entry.data[CONF_WEBFSAPI_URL] + pin = entry.data[CONF_PIN] + + afsapi = AFSAPI(webfsapi_url, pin) + + try: + await afsapi.get_power() + except FSConnectionError as exception: + raise PlatformNotReady from exception + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = afsapi + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + 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/frontier_silicon/config_flow.py b/homeassistant/components/frontier_silicon/config_flow.py new file mode 100644 index 00000000000..5e9472de62e --- /dev/null +++ b/homeassistant/components/frontier_silicon/config_flow.py @@ -0,0 +1,178 @@ +"""Config flow for Frontier Silicon Media Player integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from afsapi import AFSAPI, ConnectionError as FSConnectionError, InvalidPinException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_PIN, CONF_WEBFSAPI_URL, DEFAULT_PIN, DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + +STEP_DEVICE_CONFIG_DATA_SCHEMA = vol.Schema( + { + vol.Required( + CONF_PIN, + default=DEFAULT_PIN, + ): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Frontier Silicon Media Player.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize flow.""" + + self._webfsapi_url: str | None = None + self._name: str | None = None + self._unique_id: str | None = None + + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + """Handle the import of legacy configuration.yaml entries.""" + + device_url = f"http://{import_info[CONF_HOST]}:{import_info[CONF_PORT]}/device" + try: + self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url) + except FSConnectionError: + return self.async_abort(reason="cannot_connect") + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + return self.async_abort(reason="unknown") + + try: + afsapi = AFSAPI(self._webfsapi_url, import_info[CONF_PIN]) + + self._unique_id = await afsapi.get_radio_id() + except FSConnectionError: + return self.async_abort(reason="cannot_connect") + except InvalidPinException: + return self.async_abort(reason="invalid_auth") + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + return self.async_abort(reason="unknown") + + await self.async_set_unique_id(self._unique_id, raise_on_progress=False) + self._abort_if_unique_id_configured() + + self._name = import_info[CONF_NAME] or "Radio" + + return await self._create_entry(pin=import_info[CONF_PIN]) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step of manual configuration.""" + errors = {} + + if user_input: + device_url = ( + f"http://{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/device" + ) + try: + self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url) + except FSConnectionError: + errors["base"] = "cannot_connect" + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + errors["base"] = "unknown" + else: + return await self._async_step_device_config_if_needed() + + data_schema = self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input + ) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def _async_step_device_config_if_needed(self) -> FlowResult: + """Most users will not have changed the default PIN on their radio. + + We try to use this default PIN, and only if this fails ask for it via `async_step_device_config` + """ + + try: + # try to login with default pin + afsapi = AFSAPI(self._webfsapi_url, DEFAULT_PIN) + + self._name = await afsapi.get_friendly_name() + except InvalidPinException: + # Ask for a PIN + return await self.async_step_device_config() + + self.context["title_placeholders"] = {"name": self._name} + + self._unique_id = await afsapi.get_radio_id() + await self.async_set_unique_id(self._unique_id) + self._abort_if_unique_id_configured() + + return await self._create_entry() + + async def async_step_device_config( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle device configuration step. + + We ask for the PIN in this step. + """ + assert self._webfsapi_url is not None + + if user_input is None: + return self.async_show_form( + step_id="device_config", data_schema=STEP_DEVICE_CONFIG_DATA_SCHEMA + ) + + errors = {} + + try: + afsapi = AFSAPI(self._webfsapi_url, user_input[CONF_PIN]) + + self._name = await afsapi.get_friendly_name() + + except FSConnectionError: + errors["base"] = "cannot_connect" + except InvalidPinException: + errors["base"] = "invalid_auth" + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + errors["base"] = "unknown" + else: + self._unique_id = await afsapi.get_radio_id() + await self.async_set_unique_id(self._unique_id) + self._abort_if_unique_id_configured() + return await self._create_entry(pin=user_input[CONF_PIN]) + + data_schema = self.add_suggested_values_to_schema( + STEP_DEVICE_CONFIG_DATA_SCHEMA, user_input + ) + return self.async_show_form( + step_id="device_config", + data_schema=data_schema, + errors=errors, + ) + + async def _create_entry(self, pin: str | None = None) -> FlowResult: + """Create the entry.""" + assert self._name is not None + assert self._webfsapi_url is not None + + data = {CONF_WEBFSAPI_URL: self._webfsapi_url, CONF_PIN: pin or DEFAULT_PIN} + + return self.async_create_entry(title=self._name, data=data) diff --git a/homeassistant/components/frontier_silicon/const.py b/homeassistant/components/frontier_silicon/const.py index 9ee17c0320e..9206db89166 100644 --- a/homeassistant/components/frontier_silicon/const.py +++ b/homeassistant/components/frontier_silicon/const.py @@ -1,6 +1,9 @@ """Constants for the Frontier Silicon Media Player integration.""" DOMAIN = "frontier_silicon" +CONF_WEBFSAPI_URL = "webfsapi_url" +CONF_PIN = "pin" + DEFAULT_PIN = "1234" DEFAULT_PORT = 80 diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 322c1b90b26..62e7e617034 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,6 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "codeowners": ["@wlcrs"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "iot_class": "local_polling", "requirements": ["afsapi==0.2.7"] diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 0e3eb168484..b05ba272a19 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -21,15 +21,17 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .browse_media import browse_node, browse_top_level -from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET +from .const import CONF_PIN, DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET _LOGGER = logging.getLogger(__name__) @@ -49,7 +51,11 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Frontier Silicon platform.""" + """Set up the Frontier Silicon platform. + + YAML is deprecated, and imported automatically. + SSDP discovery is temporarily retained - to be refactor subsequently. + """ if discovery_info is not None: webfsapi_url = await AFSAPI.get_webfsapi_endpoint( discovery_info["ssdp_description"] @@ -61,24 +67,41 @@ async def async_setup_platform( [AFSAPIDevice(name, afsapi)], True, ) + return - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - password = config.get(CONF_PASSWORD) - name = config.get(CONF_NAME) + ir.async_create_issue( + hass, + DOMAIN, + "remove_yaml", + breaks_in_ha_version="2023.6.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="removed_yaml", + ) - try: - webfsapi_url = await AFSAPI.get_webfsapi_endpoint( - f"http://{host}:{port}/device" - ) - except FSConnectionError: - _LOGGER.error( - "Could not add the FSAPI device at %s:%s -> %s", host, port, password - ) - return - afsapi = AFSAPI(webfsapi_url, password) - async_add_entities([AFSAPIDevice(name, afsapi)], True) + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_NAME: config.get(CONF_NAME), + CONF_HOST: config.get(CONF_HOST), + CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT), + CONF_PIN: config.get(CONF_PASSWORD, DEFAULT_PIN), + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Frontier Silicon entity.""" + + afsapi: AFSAPI = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities([AFSAPIDevice(config_entry.title, afsapi)], True) class AFSAPIDevice(MediaPlayerEntity): diff --git a/homeassistant/components/frontier_silicon/strings.json b/homeassistant/components/frontier_silicon/strings.json new file mode 100644 index 00000000000..85b0b6958af --- /dev/null +++ b/homeassistant/components/frontier_silicon/strings.json @@ -0,0 +1,35 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "title": "Frontier Silicon Setup", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + }, + "device_config": { + "title": "Device Configuration", + "description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + } + }, + "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_device%]" + } + }, + "issues": { + "removed_yaml": { + "title": "The Frontier Silicon YAML configuration has been removed", + "description": "Configuring Frontier Silicon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8e13dd971e5..6656972f8b0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -145,6 +145,7 @@ FLOWS = { "fritzbox", "fritzbox_callmonitor", "fronius", + "frontier_silicon", "fully_kiosk", "garages_amsterdam", "gdacs", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e8350284f13..9742af1edfc 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1818,7 +1818,7 @@ "frontier_silicon": { "name": "Frontier Silicon", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "fully_kiosk": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a241892ff52..1dfb0466be6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,6 +78,9 @@ adguardhome==0.6.1 # homeassistant.components.advantage_air advantage_air==0.4.1 +# homeassistant.components.frontier_silicon +afsapi==0.2.7 + # homeassistant.components.agent_dvr agent-py==0.0.23 diff --git a/tests/components/frontier_silicon/__init__.py b/tests/components/frontier_silicon/__init__.py new file mode 100644 index 00000000000..6a039dc29ac --- /dev/null +++ b/tests/components/frontier_silicon/__init__.py @@ -0,0 +1 @@ +"""Tests for the Frontier Silicon integration.""" diff --git a/tests/components/frontier_silicon/conftest.py b/tests/components/frontier_silicon/conftest.py new file mode 100644 index 00000000000..40a6df85310 --- /dev/null +++ b/tests/components/frontier_silicon/conftest.py @@ -0,0 +1,59 @@ +"""Configuration for frontier_silicon tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.frontier_silicon.const import ( + CONF_PIN, + CONF_WEBFSAPI_URL, + DOMAIN, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_entry() -> MockConfigEntry: + """Create a mock Frontier Silicon config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="mock_radio_id", + data={CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", CONF_PIN: "1234"}, + ) + + +@pytest.fixture(autouse=True) +def mock_valid_device_url() -> Generator[None, None, None]: + """Return a valid webfsapi endpoint.""" + with patch( + "afsapi.AFSAPI.get_webfsapi_endpoint", + return_value="http://1.1.1.1:80/webfsapi", + ): + yield + + +@pytest.fixture(autouse=True) +def mock_valid_pin() -> Generator[None, None, None]: + """Make get_friendly_name return a value, indicating a valid pin.""" + with patch( + "afsapi.AFSAPI.get_friendly_name", + return_value="Name of the device", + ): + yield + + +@pytest.fixture(autouse=True) +def mock_radio_id() -> Generator[None, None, None]: + """Return a valid radio_id.""" + with patch("afsapi.AFSAPI.get_radio_id", return_value="mock_radio_id"): + yield + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.frontier_silicon.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/frontier_silicon/test_config_flow.py b/tests/components/frontier_silicon/test_config_flow.py new file mode 100644 index 00000000000..a643b121c74 --- /dev/null +++ b/tests/components/frontier_silicon/test_config_flow.py @@ -0,0 +1,266 @@ +"""Test the Frontier Silicon config flow.""" +from unittest.mock import AsyncMock, patch + +from afsapi import ConnectionError, InvalidPinException +import pytest + +from homeassistant import config_entries +from homeassistant.components.frontier_silicon.const import CONF_WEBFSAPI_URL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_import_success(hass: HomeAssistant) -> None: + """Test successful import.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Test name" + assert result["data"] == { + CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", + CONF_PIN: "1234", + } + + +@pytest.mark.parametrize( + ("webfsapi_endpoint_error", "result_reason"), + [ + (ConnectionError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_import_webfsapi_endpoint_failures( + hass: HomeAssistant, webfsapi_endpoint_error: Exception, result_reason: str +) -> None: + """Test various failure of get_webfsapi_endpoint.""" + with patch( + "afsapi.AFSAPI.get_webfsapi_endpoint", + side_effect=webfsapi_endpoint_error, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == result_reason + + +@pytest.mark.parametrize( + ("radio_id_error", "result_reason"), + [ + (ConnectionError, "cannot_connect"), + (InvalidPinException, "invalid_auth"), + (ValueError, "unknown"), + ], +) +async def test_import_radio_id_failures( + hass: HomeAssistant, radio_id_error: Exception, result_reason: str +) -> None: + """Test various failure of get_radio_id.""" + with patch( + "afsapi.AFSAPI.get_radio_id", + side_effect=radio_id_error, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == result_reason + + +async def test_import_already_exists( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test import of device which already exists.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_form_default_pin( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test manual device add with default pin.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Name of the device" + assert result2["data"] == { + CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", + CONF_PIN: "1234", + } + mock_setup_entry.assert_called_once() + + +async def test_form_nondefault_pin( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "afsapi.AFSAPI.get_friendly_name", + side_effect=InvalidPinException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "device_config" + assert result2["errors"] is None + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PIN: "4321"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Name of the device" + assert result3["data"] == { + "webfsapi_url": "http://1.1.1.1:80/webfsapi", + "pin": "4321", + } + mock_setup_entry.assert_called_once() + + +@pytest.mark.parametrize( + ("friendly_name_error", "result_error"), + [ + (ConnectionError, "cannot_connect"), + (InvalidPinException, "invalid_auth"), + (ValueError, "unknown"), + ], +) +async def test_form_nondefault_pin_invalid( + hass: HomeAssistant, friendly_name_error: Exception, result_error: str +) -> None: + """Test we get the proper errors when trying to validate an user-provided PIN.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "afsapi.AFSAPI.get_friendly_name", + side_effect=InvalidPinException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "device_config" + assert result2["errors"] is None + + with patch( + "afsapi.AFSAPI.get_friendly_name", + side_effect=friendly_name_error, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PIN: "4321"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.FORM + assert result2["step_id"] == "device_config" + assert result3["errors"] == {"base": result_error} + + +@pytest.mark.parametrize( + ("webfsapi_endpoint_error", "result_error"), + [ + (ConnectionError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_invalid_device_url( + hass: HomeAssistant, webfsapi_endpoint_error: Exception, result_error: str +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "afsapi.AFSAPI.get_webfsapi_endpoint", + side_effect=webfsapi_endpoint_error, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": result_error} From a0f725dfcb1b640564f0939e3298b2936da1a930 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:06:50 +0100 Subject: [PATCH 05/16] Add type hints to tests (#89497) --- tests/components/ecobee/test_climate.py | 8 +++--- tests/components/energyzero/test_sensor.py | 1 - tests/components/hassio/test_ingress.py | 2 +- .../homeassistant_alerts/test_init.py | 2 +- .../homematicip_cloud/test_helpers.py | 2 +- .../components/homematicip_cloud/test_lock.py | 11 ++++++-- tests/components/pjlink/test_media_player.py | 28 +++++++++++-------- tests/components/prosegur/test_camera.py | 21 +++++++++++--- tests/components/prosegur/test_diagnostics.py | 11 ++++++-- tests/components/recorder/test_statistics.py | 8 +++--- tests/components/template/test_cover.py | 2 +- tests/components/tibber/test_config_flow.py | 8 ++++-- tests/components/todoist/test_calendar.py | 4 ++- tests/components/twentemilieu/test_sensor.py | 1 - .../components/universal/test_media_player.py | 4 +-- .../components/weather/test_websocket_api.py | 6 +++- tests/components/zha/test_gateway.py | 4 +-- tests/components/zha/test_registries.py | 4 +-- tests/components/zwave_js/test_api.py | 16 ++++++----- tests/components/zwave_js/test_discovery.py | 8 ++++-- tests/helpers/test_service.py | 16 +++++------ tests/test_core.py | 2 +- tests/util/yaml/test_init.py | 6 ++-- 23 files changed, 109 insertions(+), 66 deletions(-) diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 75722d68c0c..09b127432db 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -86,7 +86,7 @@ async def test_name(thermostat) -> None: assert thermostat.name == "Ecobee" -async def test_aux_heat_not_supported_by_default(hass): +async def test_aux_heat_not_supported_by_default(hass: HomeAssistant) -> None: """Default setup should not support Aux heat.""" await setup_platform(hass, const.Platform.CLIMATE) state = hass.states.get(ENTITY_ID) @@ -100,7 +100,7 @@ async def test_aux_heat_not_supported_by_default(hass): ) -async def test_aux_heat_supported_with_heat_pump(hass): +async def test_aux_heat_supported_with_heat_pump(hass: HomeAssistant) -> None: """Aux Heat should be supported if thermostat has heatpump.""" mock_get_thermostat = mock.Mock() mock_get_thermostat.return_value = GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP @@ -242,7 +242,7 @@ async def test_extra_state_attributes(ecobee_fixture, thermostat) -> None: } == thermostat.extra_state_attributes -async def test_is_aux_heat_on(hass): +async def test_is_aux_heat_on(hass: HomeAssistant) -> None: """Test aux heat property is only enabled for auxHeatOnly.""" mock_get_thermostat = mock.Mock() mock_get_thermostat.return_value = copy.deepcopy( @@ -255,7 +255,7 @@ async def test_is_aux_heat_on(hass): assert state.attributes[climate.ATTR_AUX_HEAT] == "on" -async def test_is_aux_heat_off(hass): +async def test_is_aux_heat_off(hass: HomeAssistant) -> None: """Test aux heat property is only enabled for auxHeatOnly.""" mock_get_thermostat = mock.Mock() mock_get_thermostat.return_value = GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP diff --git a/tests/components/energyzero/test_sensor.py b/tests/components/energyzero/test_sensor.py index 4e961d1a68e..466e754df27 100644 --- a/tests/components/energyzero/test_sensor.py +++ b/tests/components/energyzero/test_sensor.py @@ -1,5 +1,4 @@ """Tests for the sensors provided by the EnergyZero integration.""" - from unittest.mock import MagicMock from energyzero import EnergyZeroNoDataError diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 67548a19c2c..06b7523614c 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -335,7 +335,7 @@ async def test_ingress_missing_peername( async def test_forwarding_paths_as_requested( - hassio_noauth_client, aioclient_mock + hassio_noauth_client, aioclient_mock: AiohttpClientMocker ) -> None: """Test incomnig URLs with double encoding go out as dobule encoded.""" # This double encoded string should be forwarded double-encoded too. diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index f5e040aa389..36f0cad7588 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -283,7 +283,7 @@ async def test_alerts( ) async def test_alerts_refreshed_on_component_load( hass: HomeAssistant, - hass_ws_client, + hass_ws_client: WebSocketGenerator, aioclient_mock: AiohttpClientMocker, ha_version, supervisor_info, diff --git a/tests/components/homematicip_cloud/test_helpers.py b/tests/components/homematicip_cloud/test_helpers.py index 85c16255d71..40ce5e536b1 100644 --- a/tests/components/homematicip_cloud/test_helpers.py +++ b/tests/components/homematicip_cloud/test_helpers.py @@ -5,7 +5,7 @@ import json from homeassistant.components.homematicip_cloud.helpers import is_error_response -async def test_is_error_response(): +async def test_is_error_response() -> None: """Test, if an response is a normal result or an error.""" assert not is_error_response("True") assert not is_error_response(True) diff --git a/tests/components/homematicip_cloud/test_lock.py b/tests/components/homematicip_cloud/test_lock.py index 48ae02738a6..61457fd5119 100644 --- a/tests/components/homematicip_cloud/test_lock.py +++ b/tests/components/homematicip_cloud/test_lock.py @@ -12,13 +12,14 @@ from homeassistant.components.lock import ( LockEntityFeature, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass): +async def test_manually_configured_platform(hass: HomeAssistant) -> None: """Test that we do not set up an access point.""" assert await async_setup_component( hass, DOMAIN, {DOMAIN: {"platform": HMIPC_DOMAIN}} @@ -26,7 +27,9 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_doorlockdrive(hass, default_mock_hap_factory): +async def test_hmip_doorlockdrive( + hass: HomeAssistant, default_mock_hap_factory +) -> None: """Test HomematicipDoorLockDrive.""" entity_id = "lock.haustuer" entity_name = "Haustuer" @@ -82,7 +85,9 @@ async def test_hmip_doorlockdrive(hass, default_mock_hap_factory): assert ha_state.state == STATE_UNLOCKING -async def test_hmip_doorlockdrive_handle_errors(hass, default_mock_hap_factory): +async def test_hmip_doorlockdrive_handle_errors( + hass: HomeAssistant, default_mock_hap_factory +) -> None: """Test HomematicipDoorLockDrive.""" entity_id = "lock.haustuer" entity_name = "Haustuer" diff --git a/tests/components/pjlink/test_media_player.py b/tests/components/pjlink/test_media_player.py index c4a923c16ee..686ece5b7ec 100644 --- a/tests/components/pjlink/test_media_player.py +++ b/tests/components/pjlink/test_media_player.py @@ -1,5 +1,4 @@ """Test the pjlink media player platform.""" - from datetime import timedelta import socket from unittest.mock import create_autospec, patch @@ -11,6 +10,7 @@ import pytest import homeassistant.components.media_player as media_player from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -48,7 +48,9 @@ def mocked_projector(projector_from_address): @pytest.mark.parametrize("side_effect", [socket.timeout, OSError]) -async def test_offline_initialization(projector_from_address, hass, side_effect): +async def test_offline_initialization( + projector_from_address, hass: HomeAssistant, side_effect +) -> None: """Test initialization of a device that is offline.""" with assert_setup_component(1, media_player.DOMAIN): @@ -71,7 +73,7 @@ async def test_offline_initialization(projector_from_address, hass, side_effect) assert state.state == "unavailable" -async def test_initialization(projector_from_address, hass): +async def test_initialization(projector_from_address, hass: HomeAssistant) -> None: """Test a device that is available.""" with assert_setup_component(1, media_player.DOMAIN): @@ -108,7 +110,9 @@ async def test_initialization(projector_from_address, hass): @pytest.mark.parametrize("power_state", ["on", "warm-up"]) -async def test_on_state_init(projector_from_address, hass, power_state): +async def test_on_state_init( + projector_from_address, hass: HomeAssistant, power_state +) -> None: """Test a device that is available.""" with assert_setup_component(1, media_player.DOMAIN): @@ -139,7 +143,7 @@ async def test_on_state_init(projector_from_address, hass, power_state): assert state.attributes["source"] == "HDMI 1" -async def test_api_error(projector_from_address, hass): +async def test_api_error(projector_from_address, hass: HomeAssistant) -> None: """Test invalid api responses.""" with assert_setup_component(1, media_player.DOMAIN): @@ -171,7 +175,7 @@ async def test_api_error(projector_from_address, hass): assert state.state == "off" -async def test_update_unavailable(projector_from_address, hass): +async def test_update_unavailable(projector_from_address, hass: HomeAssistant) -> None: """Test update to a device that is unavailable.""" with assert_setup_component(1, media_player.DOMAIN): @@ -209,7 +213,7 @@ async def test_update_unavailable(projector_from_address, hass): assert state.state == "unavailable" -async def test_unavailable_time(mocked_projector, hass): +async def test_unavailable_time(mocked_projector, hass: HomeAssistant) -> None: """Test unavailable time projector error.""" assert await async_setup_component( @@ -240,7 +244,7 @@ async def test_unavailable_time(mocked_projector, hass): assert "is_volume_muted" not in state.attributes -async def test_turn_off(mocked_projector, hass): +async def test_turn_off(mocked_projector, hass: HomeAssistant) -> None: """Test turning off beamer.""" assert await async_setup_component( @@ -265,7 +269,7 @@ async def test_turn_off(mocked_projector, hass): mocked_projector.set_power.assert_called_with("off") -async def test_turn_on(mocked_projector, hass): +async def test_turn_on(mocked_projector, hass: HomeAssistant) -> None: """Test turning on beamer.""" assert await async_setup_component( @@ -290,7 +294,7 @@ async def test_turn_on(mocked_projector, hass): mocked_projector.set_power.assert_called_with("on") -async def test_mute(mocked_projector, hass): +async def test_mute(mocked_projector, hass: HomeAssistant) -> None: """Test muting beamer.""" assert await async_setup_component( @@ -315,7 +319,7 @@ async def test_mute(mocked_projector, hass): mocked_projector.set_mute.assert_called_with(MUTE_AUDIO, True) -async def test_unmute(mocked_projector, hass): +async def test_unmute(mocked_projector, hass: HomeAssistant) -> None: """Test unmuting beamer.""" assert await async_setup_component( @@ -340,7 +344,7 @@ async def test_unmute(mocked_projector, hass): mocked_projector.set_mute.assert_called_with(MUTE_AUDIO, False) -async def test_select_source(mocked_projector, hass): +async def test_select_source(mocked_projector, hass: HomeAssistant) -> None: """Test selecting source.""" assert await async_setup_component( diff --git a/tests/components/prosegur/test_camera.py b/tests/components/prosegur/test_camera.py index 40ab57e088b..ba2e478f5cd 100644 --- a/tests/components/prosegur/test_camera.py +++ b/tests/components/prosegur/test_camera.py @@ -9,10 +9,11 @@ from homeassistant.components import camera from homeassistant.components.camera import Image from homeassistant.components.prosegur.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -async def test_camera(hass, init_integration): +async def test_camera(hass: HomeAssistant, init_integration) -> None: """Test prosegur get_image.""" image = await camera.async_get_image(hass, "camera.test_cam") @@ -20,7 +21,12 @@ async def test_camera(hass, init_integration): assert image == Image(content_type="image/jpeg", content=b"ABC") -async def test_camera_fail(hass, init_integration, mock_install, caplog): +async def test_camera_fail( + hass: HomeAssistant, + init_integration, + mock_install, + caplog: pytest.LogCaptureFixture, +) -> None: """Test prosegur get_image fails.""" mock_install.get_image = AsyncMock( @@ -37,7 +43,9 @@ async def test_camera_fail(hass, init_integration, mock_install, caplog): assert "Image test_cam doesn't exist" in caplog.text -async def test_request_image(hass, init_integration, mock_install): +async def test_request_image( + hass: HomeAssistant, init_integration, mock_install +) -> None: """Test the camera request image service.""" await hass.services.async_call( @@ -50,7 +58,12 @@ async def test_request_image(hass, init_integration, mock_install): assert mock_install.request_image.called -async def test_request_image_fail(hass, init_integration, mock_install, caplog): +async def test_request_image_fail( + hass: HomeAssistant, + init_integration, + mock_install, + caplog: pytest.LogCaptureFixture, +) -> None: """Test the camera request image service fails.""" mock_install.request_image = AsyncMock(side_effect=ProsegurException()) diff --git a/tests/components/prosegur/test_diagnostics.py b/tests/components/prosegur/test_diagnostics.py index 85377833a74..daa92de1aa0 100644 --- a/tests/components/prosegur/test_diagnostics.py +++ b/tests/components/prosegur/test_diagnostics.py @@ -1,11 +1,18 @@ """Test Prosegur diagnostics.""" - from unittest.mock import patch +from homeassistant.core import HomeAssistant + from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator -async def test_diagnostics(hass, hass_client, init_integration, mock_install): +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration, + mock_install, +) -> None: """Test generating diagnostics for a config entry.""" with patch( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index e6ae291264f..7c064a03edf 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1805,7 +1805,7 @@ def record_states(hass): return zero, four, states -def test_cache_key_for_generate_statistics_during_period_stmt(): +def test_cache_key_for_generate_statistics_during_period_stmt() -> None: """Test cache key for _generate_statistics_during_period_stmt.""" columns = select(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start_ts) stmt = _generate_statistics_during_period_stmt( @@ -1835,7 +1835,7 @@ def test_cache_key_for_generate_statistics_during_period_stmt(): assert cache_key_1 != cache_key_3 -def test_cache_key_for_generate_get_metadata_stmt(): +def test_cache_key_for_generate_get_metadata_stmt() -> None: """Test cache key for _generate_get_metadata_stmt.""" stmt_mean = _generate_get_metadata_stmt([0], "mean") stmt_mean2 = _generate_get_metadata_stmt([1], "mean") @@ -1846,7 +1846,7 @@ def test_cache_key_for_generate_get_metadata_stmt(): assert stmt_mean._generate_cache_key() != stmt_none._generate_cache_key() -def test_cache_key_for_generate_max_mean_min_statistic_in_sub_period_stmt(): +def test_cache_key_for_generate_max_mean_min_statistic_in_sub_period_stmt() -> None: """Test cache key for _generate_max_mean_min_statistic_in_sub_period_stmt.""" columns = select(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start_ts) stmt = _generate_max_mean_min_statistic_in_sub_period_stmt( @@ -1883,7 +1883,7 @@ def test_cache_key_for_generate_max_mean_min_statistic_in_sub_period_stmt(): assert cache_key_1 != cache_key_3 -def test_cache_key_for_generate_statistics_at_time_stmt(): +def test_cache_key_for_generate_statistics_at_time_stmt() -> None: """Test cache key for _generate_statistics_at_time_stmt.""" columns = select(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start_ts) stmt = _generate_statistics_at_time_stmt(columns, StatisticsShortTerm, {0}, 0.0) diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index acf49eb5469..adc41fe717b 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -225,7 +225,7 @@ async def test_template_position(hass: HomeAssistant, start_ha) -> None: }, ], ) -async def test_template_not_optimistic(hass, start_ha): +async def test_template_not_optimistic(hass: HomeAssistant, start_ha) -> None: """Test the is_closed attribute.""" state = hass.states.get("cover.test_template_cover") assert state.state == STATE_UNKNOWN diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index e07e4d66cd2..545a79ff56f 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -70,7 +70,9 @@ async def test_create_entry(recorder_mock: Recorder, hass: HomeAssistant) -> Non (FatalHttpException(404), ERR_CLIENT), ], ) -async def test_create_entry_exceptions(recorder_mock, hass, exception, expected_error): +async def test_create_entry_exceptions( + recorder_mock: Recorder, hass: HomeAssistant, exception, expected_error +) -> None: """Test create entry from user input.""" test_data = { CONF_ACCESS_TOKEN: "valid", @@ -93,7 +95,9 @@ async def test_create_entry_exceptions(recorder_mock, hass, exception, expected_ assert result["errors"][CONF_ACCESS_TOKEN] == expected_error -async def test_flow_entry_already_exists(recorder_mock, hass, config_entry): +async def test_flow_entry_already_exists( + recorder_mock: Recorder, hass: HomeAssistant, config_entry +) -> None: """Test user input for config_entry that already exists.""" test_data = { CONF_ACCESS_TOKEN: "valid", diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 5b5dc817d6d..82eff0d7553 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -91,7 +91,9 @@ async def test_calendar_entity_unique_id( @patch("homeassistant.components.todoist.calendar.TodoistAPIAsync") -async def test_update_entity_for_custom_project_with_labels_on(todoist_api, hass, api): +async def test_update_entity_for_custom_project_with_labels_on( + todoist_api, hass: HomeAssistant, api +) -> None: """Test that the calendar's state is on for a custom project using labels.""" todoist_api.return_value = api assert await setup.async_setup_component( diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index 6fd39e38d48..e4b845264db 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -1,5 +1,4 @@ """Tests for the Twente Milieu sensors.""" - import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 12d7b444097..fbf4e576dd5 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1103,7 +1103,7 @@ async def test_state_template(hass: HomeAssistant) -> None: assert hass.states.get("media_player.tv").state == STATE_OFF -async def test_browse_media(hass: HomeAssistant): +async def test_browse_media(hass: HomeAssistant) -> None: """Test browse media.""" await async_setup_component( hass, "media_player", {"media_player": {"platform": "demo"}} @@ -1133,7 +1133,7 @@ async def test_browse_media(hass: HomeAssistant): assert result == MOCK_BROWSE_MEDIA -async def test_browse_media_override(hass: HomeAssistant): +async def test_browse_media_override(hass: HomeAssistant) -> None: """Test browse media override.""" await async_setup_component( hass, "media_player", {"media_player": {"platform": "demo"}} diff --git a/tests/components/weather/test_websocket_api.py b/tests/components/weather/test_websocket_api.py index 1112d7713ed..760acbb2bb0 100644 --- a/tests/components/weather/test_websocket_api.py +++ b/tests/components/weather/test_websocket_api.py @@ -3,8 +3,12 @@ from homeassistant.components.weather.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.typing import WebSocketGenerator -async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None: + +async def test_device_class_units( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: """Test we can get supported units.""" assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index adff43d377b..392c589ea18 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -306,8 +306,8 @@ async def test_gateway_initialize_failure_transient( ], ) async def test_gateway_initialize_bellows_thread( - device_path, thread_state, config_override, hass, coordinator -): + device_path, thread_state, config_override, hass: HomeAssistant, coordinator +) -> None: """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 24cd7a5785f..6a6bf758ceb 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -323,7 +323,7 @@ def test_weighted_match( model, quirk_class, match_name, -): +) -> None: """Test weightedd match.""" s = mock.sentinel @@ -435,7 +435,7 @@ def test_multi_sensor_match(channel, entity_registry: er.EntityRegistry) -> None } -def test_quirk_classes(): +def test_quirk_classes() -> None: """Make sure that quirk_classes in components matches are valid.""" def find_quirk_class(base_obj, quirk_mod, quirk_cls): diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 4e99d19261b..43489be4ccf 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1460,8 +1460,8 @@ async def test_parse_qr_code_string( async def test_try_parse_dsk_from_qr_code_string( - hass, integration, client, hass_ws_client -): + hass: HomeAssistant, integration, client, hass_ws_client: WebSocketGenerator +) -> None: """Test try_parse_dsk_from_qr_code_string websocket command.""" entry = integration ws_client = await hass_ws_client(hass) @@ -1524,7 +1524,9 @@ async def test_try_parse_dsk_from_qr_code_string( assert msg["error"]["code"] == ERR_NOT_LOADED -async def test_supports_feature(hass, integration, client, hass_ws_client): +async def test_supports_feature( + hass: HomeAssistant, integration, client, hass_ws_client: WebSocketGenerator +) -> None: """Test supports_feature websocket command.""" entry = integration ws_client = await hass_ws_client(hass) @@ -3888,8 +3890,8 @@ async def test_subscribe_firmware_update_status_initial_value( async def test_subscribe_controller_firmware_update_status( - hass, integration, client, hass_ws_client -): + hass: HomeAssistant, integration, client, hass_ws_client: WebSocketGenerator +) -> None: """Test the subscribe_firmware_update_status websocket command for a node.""" ws_client = await hass_ws_client(hass) device = get_device(hass, client.driver.controller.nodes[1]) @@ -3954,8 +3956,8 @@ async def test_subscribe_controller_firmware_update_status( async def test_subscribe_controller_firmware_update_status_initial_value( - hass, client, integration, hass_ws_client -): + hass: HomeAssistant, client, integration, hass_ws_client: WebSocketGenerator +) -> None: """Test subscribe_firmware_update_status cmd with in progress update for node.""" ws_client = await hass_ws_client(hass) device = get_device(hass, client.driver.controller.nodes[1]) diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 66969c51ff0..1840e4d7980 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -103,7 +103,9 @@ async def test_dynamic_climate_data_discovery_template_failure( ) -async def test_merten_507801(hass, client, merten_507801, integration): +async def test_merten_507801( + hass: HomeAssistant, client, merten_507801, integration +) -> None: """Test that Merten 507801 multilevel switch value is discovered as a cover.""" node = merten_507801 assert node.device_class.specific.label == "Unused" @@ -116,8 +118,8 @@ async def test_merten_507801(hass, client, merten_507801, integration): async def test_merten_507801_disabled_enitites( - hass, client, merten_507801, integration -): + hass: HomeAssistant, client, merten_507801, integration +) -> None: """Test that Merten 507801 entities created by endpoint 2 are disabled.""" registry = er.async_get(hass) entity_ids = [ diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 43bbf85b06c..ff86f9c7e76 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -223,7 +223,7 @@ def area_mock(hass): ) -async def test_call_from_config(hass: HomeAssistant): +async def test_call_from_config(hass: HomeAssistant) -> None: """Test the sync wrapper of service.async_call_from_config.""" calls = async_mock_service(hass, "test_domain", "test_service") config = { @@ -238,7 +238,7 @@ async def test_call_from_config(hass: HomeAssistant): assert calls[0].data == {"hello": "goodbye", "entity_id": ["hello.world"]} -async def test_service_call(hass: HomeAssistant): +async def test_service_call(hass: HomeAssistant) -> None: """Test service call with templating.""" calls = async_mock_service(hass, "test_domain", "test_service") config = { @@ -307,7 +307,7 @@ async def test_service_call(hass: HomeAssistant): } -async def test_service_template_service_call(hass: HomeAssistant): +async def test_service_template_service_call(hass: HomeAssistant) -> None: """Test legacy service_template call with templating.""" calls = async_mock_service(hass, "test_domain", "test_service") config = { @@ -322,7 +322,7 @@ async def test_service_template_service_call(hass: HomeAssistant): assert calls[0].data == {"hello": "goodbye", "entity_id": ["hello.world"]} -async def test_passing_variables_to_templates(hass: HomeAssistant): +async def test_passing_variables_to_templates(hass: HomeAssistant) -> None: """Test passing variables to templates.""" calls = async_mock_service(hass, "test_domain", "test_service") config = { @@ -344,7 +344,7 @@ async def test_passing_variables_to_templates(hass: HomeAssistant): assert calls[0].data == {"hello": "goodbye", "entity_id": ["hello.world"]} -async def test_bad_template(hass: HomeAssistant): +async def test_bad_template(hass: HomeAssistant) -> None: """Test passing bad template.""" calls = async_mock_service(hass, "test_domain", "test_service") config = { @@ -366,7 +366,7 @@ async def test_bad_template(hass: HomeAssistant): assert len(calls) == 0 -async def test_split_entity_string(hass: HomeAssistant): +async def test_split_entity_string(hass: HomeAssistant) -> None: """Test splitting of entity string.""" calls = async_mock_service(hass, "test_domain", "test_service") await service.async_call_from_config( @@ -380,7 +380,7 @@ async def test_split_entity_string(hass: HomeAssistant): assert ["hello.world", "sensor.beer"] == calls[-1].data.get("entity_id") -async def test_not_mutate_input(hass: HomeAssistant): +async def test_not_mutate_input(hass: HomeAssistant) -> None: """Test for immutable input.""" async_mock_service(hass, "test_domain", "test_service") config = { @@ -403,7 +403,7 @@ async def test_not_mutate_input(hass: HomeAssistant): @patch("homeassistant.helpers.service._LOGGER.error") -async def test_fail_silently_if_no_service(mock_log, hass: HomeAssistant): +async def test_fail_silently_if_no_service(mock_log, hass: HomeAssistant) -> None: """Test failing if service is missing.""" await service.async_call_from_config(hass, None) assert mock_log.call_count == 1 diff --git a/tests/test_core.py b/tests/test_core.py index 6d67376b418..6167bf6a63b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -75,7 +75,7 @@ def test_async_add_hass_job_schedule_callback() -> None: assert len(hass.add_job.mock_calls) == 0 -def test_async_add_hass_job_coro_named(hass) -> None: +def test_async_add_hass_job_coro_named(hass: HomeAssistant) -> None: """Test that we schedule coroutines and add jobs to the job pool with a name.""" async def mycoro(): diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 28ccdcc5893..bd99889234f 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -492,7 +492,9 @@ def test_representing_yaml_loaded_data( @pytest.mark.parametrize("hass_config_yaml", ["key: thing1\nkey: thing2"]) -def test_duplicate_key(caplog, try_both_loaders, mock_hass_config_yaml: None) -> None: +def test_duplicate_key( + caplog: pytest.LogCaptureFixture, try_both_loaders, mock_hass_config_yaml: None +) -> None: """Test duplicate dict keys.""" load_yaml_config_file(YAML_CONFIG_FILE) assert "contains duplicate key" in caplog.text @@ -503,7 +505,7 @@ def test_duplicate_key(caplog, try_both_loaders, mock_hass_config_yaml: None) -> [{YAML_CONFIG_FILE: "key: !secret a", yaml.SECRET_YAML: "a: 1\nb: !secret a"}], ) def test_no_recursive_secrets( - caplog, try_both_loaders, mock_hass_config_yaml: None + caplog: pytest.LogCaptureFixture, try_both_loaders, mock_hass_config_yaml: None ) -> None: """Test that loading of secrets from the secrets file fails correctly.""" with pytest.raises(HomeAssistantError) as e: From 0f15f8b84b51121f4f63136d6da86e426f80fe96 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:58:29 +0100 Subject: [PATCH 06/16] Bump pytest-sugar to 0.9.6 (#89500) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 10ceb81365d..51ce5c7d123 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -24,7 +24,7 @@ pytest-cov==3.0.0 pytest-freezer==0.4.6 pytest-socket==0.5.1 pytest-test-groups==1.0.3 -pytest-sugar==0.9.5 +pytest-sugar==0.9.6 pytest-timeout==2.1.0 pytest-unordered==0.5.2 pytest-picked==0.4.6 From b4c1c0beb7535e99cf05ee146860f3262917322b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 13:08:45 +0100 Subject: [PATCH 07/16] Bump pytest-xdist to 3.2.0 (#89501) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 51ce5c7d123..7601326cd56 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -28,7 +28,7 @@ pytest-sugar==0.9.6 pytest-timeout==2.1.0 pytest-unordered==0.5.2 pytest-picked==0.4.6 -pytest-xdist==2.5.0 +pytest-xdist==3.2.0 pytest==7.2.2 requests_mock==1.10.0 respx==0.20.1 From f4b8598979219038f98593faf0a2f09b7aad8889 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 13:27:07 +0100 Subject: [PATCH 08/16] Bump home-assistant/builder from 2022.11.0 to 2023.03.0 (#89485) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index cc87e4708c7..45774642a5c 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -198,7 +198,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.11.0 + uses: home-assistant/builder@2023.03.0 with: args: | $BUILD_ARGS \ @@ -276,7 +276,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.11.0 + uses: home-assistant/builder@2023.03.0 with: args: | $BUILD_ARGS \ From 029093d0b27ce6f69f131605140e61c1ce35ce78 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:48:58 +0100 Subject: [PATCH 09/16] Fix lingering timer in device registry (#89422) --- homeassistant/helpers/device_registry.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 9ea44db16d5..b72a1878651 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, TypeVar, cast import attr from homeassistant.backports.enum import StrEnum -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, RequiredParameterMissing from homeassistant.loader import bind_hass @@ -907,6 +907,13 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) + @callback + def _on_homeassistant_stop(event: Event) -> None: + """Cancel debounced cleanup.""" + debounced_cleanup.async_cancel() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop) + def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, str]]: """Normalize connections to ensure we can match mac addresses.""" From 75bca76e6864b724854fd04e79fc8a3365319d92 Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Fri, 10 Mar 2023 15:57:35 +0100 Subject: [PATCH 10/16] Landis+Gyr move coordinator to own file (#89433) * Move coordinator to own file and add test cases * Apply typing improvements from review * Remove testcase for exception during setup * Simplify unittest for failing serial connection * Readd checks in serial connection test after review --- .../landisgyr_heat_meter/__init__.py | 18 +--- .../components/landisgyr_heat_meter/const.py | 3 + .../landisgyr_heat_meter/coordinator.py | 37 +++++++ .../landisgyr_heat_meter/test_sensor.py | 96 ++++++++++++++++--- 4 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/landisgyr_heat_meter/coordinator.py diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index eae5e91196c..541fef017d0 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -1,19 +1,17 @@ """The Landis+Gyr Heat Meter integration.""" from __future__ import annotations -from datetime import timedelta import logging import ultraheat_api -from ultraheat_api.response import HeatMeterResponse from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import async_migrate_entries -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN +from .coordinator import UltraheatCoordinator _LOGGER = logging.getLogger(__name__) @@ -27,19 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: reader = ultraheat_api.UltraheatReader(entry.data[CONF_DEVICE]) api = ultraheat_api.HeatMeterService(reader) - async def async_update_data() -> HeatMeterResponse: - """Fetch data from the API.""" - _LOGGER.debug("Polling on %s", entry.data[CONF_DEVICE]) - return await hass.async_add_executor_job(api.read) - - # Polling is only daily to prevent battery drain. - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="ultraheat_gateway", - update_method=async_update_data, - update_interval=timedelta(days=1), - ) + coordinator = UltraheatCoordinator(hass, api) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 5d27a8a1705..56f5980a839 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -1,6 +1,9 @@ """Constants for the Landis+Gyr Heat Meter integration.""" +from datetime import timedelta + DOMAIN = "landisgyr_heat_meter" GJ_TO_MWH = 0.277778 # conversion factor ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time +POLLING_INTERVAL = timedelta(days=1) # Polling is only daily to prevent battery drain. diff --git a/homeassistant/components/landisgyr_heat_meter/coordinator.py b/homeassistant/components/landisgyr_heat_meter/coordinator.py new file mode 100644 index 00000000000..c85c661e79c --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/coordinator.py @@ -0,0 +1,37 @@ +"""Data update coordinator for the ultraheat api.""" + +import logging + +import async_timeout +import serial +from ultraheat_api.response import HeatMeterResponse +from ultraheat_api.service import HeatMeterService + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import POLLING_INTERVAL, ULTRAHEAT_TIMEOUT + +_LOGGER = logging.getLogger(__name__) + + +class UltraheatCoordinator(DataUpdateCoordinator[HeatMeterResponse]): + """Coordinator for getting data from the ultraheat api.""" + + def __init__(self, hass: HomeAssistant, api: HeatMeterService) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + name="ultraheat", + update_interval=POLLING_INTERVAL, + ) + self.api = api + + async def _async_update_data(self) -> HeatMeterResponse: + """Fetch data from API endpoint.""" + try: + async with async_timeout.timeout(ULTRAHEAT_TIMEOUT): + return await self.hass.async_add_executor_job(self.api.read) + except (FileNotFoundError, serial.serialutil.SerialException) as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index 9a94491a94f..854ead82b3d 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -3,11 +3,13 @@ from dataclasses import dataclass import datetime from unittest.mock import patch +import serial + from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.landisgyr_heat_meter.const import DOMAIN +from homeassistant.components.landisgyr_heat_meter.const import DOMAIN, POLLING_INTERVAL from homeassistant.components.sensor import ( ATTR_LAST_RESET, ATTR_STATE_CLASS, @@ -19,6 +21,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, EntityCategory, UnitOfEnergy, UnitOfVolume, @@ -28,21 +31,29 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + mock_restore_cache_with_extra_data, +) + +API_HEAT_METER_SERVICE = ( + "homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService" +) @dataclass class MockHeatMeterResponse: """Mock for HeatMeterResponse.""" - heat_usage_gj: int - volume_usage_m3: int - heat_previous_year_gj: int + heat_usage_gj: float + volume_usage_m3: float + heat_previous_year_gj: float device_number: str meter_date_time: datetime.datetime -@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) async def test_create_sensors( mock_heat_meter, hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: @@ -57,9 +68,9 @@ async def test_create_sensors( mock_entry.add_to_hass(hass) mock_heat_meter_response = MockHeatMeterResponse( - heat_usage_gj=123, - volume_usage_m3=456, - heat_previous_year_gj=111, + heat_usage_gj=123.0, + volume_usage_m3=456.0, + heat_previous_year_gj=111.0, device_number="devicenr_789", meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 19, 41, 17)), ) @@ -89,7 +100,7 @@ async def test_create_sensors( state = hass.states.get("sensor.heat_meter_volume_usage") assert state - assert state.state == "456" + assert state.state == "456.0" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL @@ -110,7 +121,7 @@ async def test_create_sensors( assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC -@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) async def test_restore_state(mock_heat_meter, hass: HomeAssistant) -> None: """Test sensor restore state.""" # Home assistant is not running yet @@ -199,3 +210,66 @@ async def test_restore_state(mock_heat_meter, hass: HomeAssistant) -> None: assert state assert state.state == "devicenr_789" assert state.attributes.get(ATTR_STATE_CLASS) is None + + +@patch(API_HEAT_METER_SERVICE) +async def test_exception_on_polling(mock_heat_meter, hass: HomeAssistant) -> None: + """Test sensor.""" + entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data) + mock_entry.add_to_hass(hass) + + # First setup normally + mock_heat_meter_response = MockHeatMeterResponse( + heat_usage_gj=123.0, + volume_usage_m3=456.0, + heat_previous_year_gj=111.0, + device_number="devicenr_789", + meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 19, 41, 17)), + ) + + mock_heat_meter().read.return_value = mock_heat_meter_response + + await hass.config_entries.async_setup(mock_entry.entry_id) + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "sensor.heat_meter_heat_usage"}, + blocking=True, + ) + await hass.async_block_till_done() + + # check if initial setup succeeded + state = hass.states.get("sensor.heat_meter_heat_usage") + assert state + assert state.state == "34.16669" + + # Now 'disable' the connection and wait for polling and see if it fails + mock_heat_meter().read.side_effect = serial.serialutil.SerialException + async_fire_time_changed(hass, dt_util.utcnow() + POLLING_INTERVAL) + await hass.async_block_till_done() + state = hass.states.get("sensor.heat_meter_heat_usage") + assert state.state == STATE_UNAVAILABLE + + # Now 'enable' and see if next poll succeeds + mock_heat_meter_response = MockHeatMeterResponse( + heat_usage_gj=124.0, + volume_usage_m3=457.0, + heat_previous_year_gj=112.0, + device_number="devicenr_789", + meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 20, 41, 17)), + ) + + mock_heat_meter().read.return_value = mock_heat_meter_response + mock_heat_meter().read.side_effect = None + async_fire_time_changed(hass, dt_util.utcnow() + POLLING_INTERVAL) + await hass.async_block_till_done() + state = hass.states.get("sensor.heat_meter_heat_usage") + assert state + assert state.state == "34.44447" From f674559a71e0f7cee474f594bbe3d9c4c2288454 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 16:04:45 +0100 Subject: [PATCH 11/16] Add missing mock in landisgyr config flow tests (#89513) --- .../landisgyr_heat_meter/conftest.py | 15 ++++++++++++ .../landisgyr_heat_meter/test_config_flow.py | 23 ++++++++----------- 2 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 tests/components/landisgyr_heat_meter/conftest.py diff --git a/tests/components/landisgyr_heat_meter/conftest.py b/tests/components/landisgyr_heat_meter/conftest.py new file mode 100644 index 00000000000..711fa2110f4 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/conftest.py @@ -0,0 +1,15 @@ +"""Define fixtures for Landis + Gyr Heat Meter tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.landisgyr_heat_meter.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index 57638868647..b58c91f8f16 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from unittest.mock import patch +import pytest import serial import serial.tools.list_ports @@ -14,6 +15,8 @@ from tests.common import MockConfigEntry API_HEAT_METER_SERVICE = "homeassistant.components.landisgyr_heat_meter.config_flow.ultraheat_api.HeatMeterService" +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + def mock_serial_port(): """Mock of a serial port.""" @@ -57,13 +60,9 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} - with patch( - "homeassistant.components.landisgyr_heat_meter.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device": "/dev/ttyUSB0"} - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" @@ -122,13 +121,9 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} - with patch( - "homeassistant.components.landisgyr_heat_meter.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device": "/dev/ttyUSB0"} - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" From 401273dcfffb44648cf05b2f2c2a44dd39683ccf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 16:05:13 +0100 Subject: [PATCH 12/16] Add missing mock in lacrosse_view config flow tests (#89512) --- tests/components/lacrosse_view/conftest.py | 14 ++++++ .../lacrosse_view/test_config_flow.py | 49 +++++++++---------- 2 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 tests/components/lacrosse_view/conftest.py diff --git a/tests/components/lacrosse_view/conftest.py b/tests/components/lacrosse_view/conftest.py new file mode 100644 index 00000000000..1ea3144e4c2 --- /dev/null +++ b/tests/components/lacrosse_view/conftest.py @@ -0,0 +1,14 @@ +"""Define fixtures for LaCrosse View tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.lacrosse_view.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index 67c9bf5752c..075aa7a3767 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -1,7 +1,8 @@ """Test the LaCrosse View config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from lacrosse_view import Location, LoginError +import pytest from homeassistant import config_entries from homeassistant.components.lacrosse_view.const import DOMAIN @@ -10,8 +11,10 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry +pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_form(hass: HomeAssistant) -> None: + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -25,8 +28,6 @@ async def test_form(hass: HomeAssistant) -> None: ), patch( "lacrosse_view.LaCrosse.get_locations", return_value=[Location(id=1, name="Test")], - ), patch( - "homeassistant.components.lacrosse_view.async_setup_entry", return_value=True ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -41,17 +42,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["step_id"] == "location" assert result2["errors"] is None - with patch( - "homeassistant.components.lacrosse_view.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - "location": "1", - }, - ) - await hass.async_block_till_done() + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "location": "1", + }, + ) + await hass.async_block_till_done() assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Test" @@ -170,7 +167,9 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} -async def test_already_configured_device(hass: HomeAssistant) -> None: +async def test_already_configured_device( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test we handle invalid auth.""" mock_config_entry = MockConfigEntry( domain=DOMAIN, @@ -212,17 +211,13 @@ async def test_already_configured_device(hass: HomeAssistant) -> None: assert result2["step_id"] == "location" assert result2["errors"] is None - with patch( - "homeassistant.components.lacrosse_view.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - "location": "1", - }, - ) - await hass.async_block_till_done() + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "location": "1", + }, + ) + await hass.async_block_till_done() assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" From f22fabdd7f9baa336f378b9086e5956e765a8bcb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 16:05:31 +0100 Subject: [PATCH 13/16] Add missing mock in kmtronic config flow tests (#89511) --- tests/components/kmtronic/conftest.py | 14 ++++++++++++++ tests/components/kmtronic/test_config_flow.py | 12 ++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 tests/components/kmtronic/conftest.py diff --git a/tests/components/kmtronic/conftest.py b/tests/components/kmtronic/conftest.py new file mode 100644 index 00000000000..4310f99242e --- /dev/null +++ b/tests/components/kmtronic/conftest.py @@ -0,0 +1,14 @@ +"""Define fixtures for kmtronic tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.kmtronic.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 37a77a00000..76d11b8451e 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -1,8 +1,9 @@ """Test the kmtronic config flow.""" from http import HTTPStatus -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from aiohttp import ClientConnectorError, ClientResponseError +import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN @@ -12,8 +13,10 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker +pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_form(hass: HomeAssistant) -> None: + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -25,10 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status", return_value=[Mock()], - ), patch( - "homeassistant.components.kmtronic.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { From 74d4a26f9776cb218f3ff2d4c383fed51783dee2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 16:06:53 +0100 Subject: [PATCH 14/16] Add missing mock in jellyfin config flow tests (#89510) --- tests/components/jellyfin/test_config_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index 015d44722e0..51aa4bccc92 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -1,6 +1,8 @@ """Test the jellyfin config flow.""" from unittest.mock import MagicMock +import pytest + from homeassistant import config_entries, data_entry_flow from homeassistant.components.jellyfin.const import CONF_CLIENT_DEVICE_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -11,6 +13,8 @@ from .const import TEST_PASSWORD, TEST_URL, TEST_USERNAME from tests.common import MockConfigEntry +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: """Check flow abort when an entry already exist.""" From 96bd7143643a813689c86f6f7512008c187a4766 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Mar 2023 16:09:04 +0100 Subject: [PATCH 15/16] Add FTTH and WAN info to SFR box diagnostics (#89492) * Add FTTH and WAN info to SFR box diagnostics * Adjust tests * Use snapshots --- .../components/sfr_box/diagnostics.py | 6 ++ tests/components/sfr_box/conftest.py | 46 ++++++++++---- .../sfr_box/fixtures/ftth_getInfo.json | 4 ++ .../sfr_box/fixtures/wan_getInfo.json | 11 ++++ .../sfr_box/snapshots/test_diagnostics.ambr | 60 +++++++++++++++++++ tests/components/sfr_box/test_diagnostics.py | 55 ++++------------- 6 files changed, 127 insertions(+), 55 deletions(-) create mode 100644 tests/components/sfr_box/fixtures/ftth_getInfo.json create mode 100644 tests/components/sfr_box/fixtures/wan_getInfo.json create mode 100644 tests/components/sfr_box/snapshots/test_diagnostics.ambr diff --git a/homeassistant/components/sfr_box/diagnostics.py b/homeassistant/components/sfr_box/diagnostics.py index 6a7ceb0e86b..60df2173968 100644 --- a/homeassistant/components/sfr_box/diagnostics.py +++ b/homeassistant/components/sfr_box/diagnostics.py @@ -27,8 +27,14 @@ async def async_get_config_entry_diagnostics( }, "data": { "dsl": async_redact_data(dataclasses.asdict(data.dsl.data), TO_REDACT), + "ftth": async_redact_data( + dataclasses.asdict(await data.system.box.ftth_get_info()), TO_REDACT + ), "system": async_redact_data( dataclasses.asdict(data.system.data), TO_REDACT ), + "wan": async_redact_data( + dataclasses.asdict(await data.system.box.wan_get_info()), TO_REDACT + ), }, } diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py index 1857ffeec30..a8cd6fd8bd4 100644 --- a/tests/components/sfr_box/conftest.py +++ b/tests/components/sfr_box/conftest.py @@ -4,7 +4,7 @@ import json from unittest.mock import AsyncMock, patch import pytest -from sfrbox_api.models import DslInfo, SystemInfo +from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry @@ -57,17 +57,6 @@ def get_config_entry_with_auth(hass: HomeAssistant) -> ConfigEntry: return config_entry_with_auth -@pytest.fixture -def system_get_info() -> Generator[SystemInfo, None, None]: - """Fixture for SFRBox.system_get_info.""" - system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) - with patch( - "homeassistant.components.sfr_box.coordinator.SFRBox.system_get_info", - return_value=system_info, - ): - yield system_info - - @pytest.fixture def dsl_get_info() -> Generator[DslInfo, None, None]: """Fixture for SFRBox.dsl_get_info.""" @@ -77,3 +66,36 @@ def dsl_get_info() -> Generator[DslInfo, None, None]: return_value=dsl_info, ): yield dsl_info + + +@pytest.fixture +def ftth_get_info() -> Generator[FtthInfo, None, None]: + """Fixture for SFRBox.ftth_get_info.""" + info = FtthInfo(**json.loads(load_fixture("ftth_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.ftth_get_info", + return_value=info, + ): + yield info + + +@pytest.fixture +def system_get_info() -> Generator[SystemInfo, None, None]: + """Fixture for SFRBox.system_get_info.""" + info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.system_get_info", + return_value=info, + ): + yield info + + +@pytest.fixture +def wan_get_info() -> Generator[WanInfo, None, None]: + """Fixture for SFRBox.wan_get_info.""" + info = WanInfo(**json.loads(load_fixture("wan_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.wan_get_info", + return_value=info, + ): + yield info diff --git a/tests/components/sfr_box/fixtures/ftth_getInfo.json b/tests/components/sfr_box/fixtures/ftth_getInfo.json new file mode 100644 index 00000000000..32f720e9111 --- /dev/null +++ b/tests/components/sfr_box/fixtures/ftth_getInfo.json @@ -0,0 +1,4 @@ +{ + "status": "down", + "wanfibre": "out" +} diff --git a/tests/components/sfr_box/fixtures/wan_getInfo.json b/tests/components/sfr_box/fixtures/wan_getInfo.json new file mode 100644 index 00000000000..fdef6270f35 --- /dev/null +++ b/tests/components/sfr_box/fixtures/wan_getInfo.json @@ -0,0 +1,11 @@ +{ + "status": "up", + "uptime": 297464, + "ip_addr": "1.2.3.4", + "infra": "adsl", + "mode": "adsl/routed", + "infra6": "", + "status6": "down", + "uptime6": null, + "ipv6_addr": "" +} diff --git a/tests/components/sfr_box/snapshots/test_diagnostics.ambr b/tests/components/sfr_box/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..2e25259268d --- /dev/null +++ b/tests/components/sfr_box/snapshots/test_diagnostics.ambr @@ -0,0 +1,60 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'dsl': dict({ + 'attenuation_down': 28.5, + 'attenuation_up': 20.8, + 'counter': 16, + 'crc': 0, + 'line_status': 'No Defect', + 'linemode': 'ADSL2+', + 'noise_down': 5.8, + 'noise_up': 6.0, + 'rate_down': 5549, + 'rate_up': 187, + 'status': 'up', + 'training': 'Showtime', + 'uptime': 450796, + }), + 'ftth': dict({ + 'status': 'down', + 'wanfibre': 'out', + }), + 'system': dict({ + 'alimvoltage': 12251, + 'current_datetime': '202212282233', + 'idur': 'RP3P85K', + 'mac_addr': '**REDACTED**', + 'net_infra': 'adsl', + 'net_mode': 'router', + 'product_id': 'NB6VAC-FXC-r0', + 'refclient': '', + 'serial_number': '**REDACTED**', + 'temperature': 27560, + 'uptime': 2353575, + 'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8', + 'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p', + 'version_mainfirmware': 'NB6VAC-MAIN-R4.0.44k', + 'version_rescuefirmware': 'NB6VAC-MAIN-R4.0.44k', + }), + 'wan': dict({ + 'infra': 'adsl', + 'infra6': '', + 'ip_addr': '1.2.3.4', + 'ipv6_addr': '', + 'mode': 'adsl/routed', + 'status': 'up', + 'status6': 'down', + 'uptime': 297464, + 'uptime6': None, + }), + }), + 'entry': dict({ + 'data': dict({ + 'host': '192.168.0.1', + }), + 'title': 'Mock Title', + }), + }) +# --- diff --git a/tests/components/sfr_box/test_diagnostics.py b/tests/components/sfr_box/test_diagnostics.py index 966a038b489..37e3ba9487f 100644 --- a/tests/components/sfr_box/test_diagnostics.py +++ b/tests/components/sfr_box/test_diagnostics.py @@ -3,15 +3,17 @@ from collections.abc import Generator from unittest.mock import patch import pytest +from syrupy.assertion import SnapshotAssertion -from homeassistant.components.diagnostics import REDACTED from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator -pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") +pytestmark = pytest.mark.usefixtures( + "dsl_get_info", "ftth_get_info", "system_get_info", "wan_get_info" +) @pytest.fixture(autouse=True) @@ -22,49 +24,16 @@ def override_platforms() -> Generator[None, None, None]: async def test_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry, hass_client: ClientSessionGenerator + hass: HomeAssistant, + config_entry: ConfigEntry, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "data": {"host": "192.168.0.1"}, - "title": "Mock Title", - }, - "data": { - "dsl": { - "attenuation_down": 28.5, - "attenuation_up": 20.8, - "counter": 16, - "crc": 0, - "line_status": "No Defect", - "linemode": "ADSL2+", - "noise_down": 5.8, - "noise_up": 6.0, - "rate_down": 5549, - "rate_up": 187, - "status": "up", - "training": "Showtime", - "uptime": 450796, - }, - "system": { - "alimvoltage": 12251, - "current_datetime": "202212282233", - "idur": "RP3P85K", - "mac_addr": REDACTED, - "net_infra": "adsl", - "net_mode": "router", - "product_id": "NB6VAC-FXC-r0", - "refclient": "", - "serial_number": REDACTED, - "temperature": 27560, - "uptime": 2353575, - "version_bootloader": "NB6VAC-BOOTLOADER-R4.0.8", - "version_dsldriver": "NB6VAC-XDSL-A2pv6F039p", - "version_mainfirmware": "NB6VAC-MAIN-R4.0.44k", - "version_rescuefirmware": "NB6VAC-MAIN-R4.0.44k", - }, - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From d6a223f0e161fbd7a1fe906351b8ffbe75fd1efa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Mar 2023 11:42:53 -0500 Subject: [PATCH 16/16] Await block till done inside patched config entry in tests (#89515) --- tests/components/advantage_air/test_config_flow.py | 2 +- tests/components/bond/test_config_flow.py | 2 +- tests/components/epson/test_config_flow.py | 2 +- tests/components/faa_delays/test_config_flow.py | 2 +- tests/components/kmtronic/test_config_flow.py | 2 +- tests/components/kostal_plenticore/test_config_flow.py | 2 +- tests/components/kulersky/test_config_flow.py | 4 ++-- tests/components/lutron_caseta/test_config_flow.py | 2 +- tests/components/nanoleaf/test_config_flow.py | 5 +++-- tests/components/nws/test_config_flow.py | 3 ++- tests/components/onewire/test_config_flow.py | 5 ++--- tests/components/syncthru/test_config_flow.py | 2 +- 12 files changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index 4783e9cb635..fc74df5538b 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -33,12 +33,12 @@ async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> result1["flow_id"], USER_INPUT, ) + await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 1 assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "testname" assert result2["data"] == USER_INPUT - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 # Test Duplicate Config Flow diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index a060def1cb6..fab579a81a3 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -187,11 +187,11 @@ async def test_user_form_one_entry_per_device_allowed(hass: HomeAssistant) -> No result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) + await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "already_configured" - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index fadfb2085b1..be0267a4af8 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -33,11 +33,11 @@ async def test_form(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "test-epson" assert result2["data"] == {CONF_HOST: "1.1.1.1"} - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index ce7f86e6060..9eb166d5f69 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -37,13 +37,13 @@ async def test_form(hass: HomeAssistant) -> None: "id": "test", }, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Test airport" assert result2["data"] == { "id": "test", } - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 76d11b8451e..ba8f2f5b87e 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -37,6 +37,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "1.1.1.1" @@ -45,7 +46,6 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: "username": "test-username", "password": "test-password", } - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 5d67ca3ae66..3c64a48c218 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -47,6 +47,7 @@ async def test_formx(hass: HomeAssistant) -> None: "password": "test-password", }, ) + await hass.async_block_till_done() mock_api_class.assert_called_once_with(ANY, "1.1.1.1") mock_api.__aenter__.assert_called_once() @@ -60,7 +61,6 @@ async def test_formx(hass: HomeAssistant) -> None: "host": "1.1.1.1", "password": "test-password", } - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index 3a26f16f3a0..a09fc78797b 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -60,10 +60,10 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: result["flow_id"], {}, ) + await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 @@ -87,8 +87,8 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: result["flow_id"], {}, ) + await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 72a0d4b71ac..cc71eb5910f 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -67,13 +67,13 @@ async def test_bridge_import_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data=entry_mock_data, ) + await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == CasetaConfigFlow.ENTRY_DEFAULT_TITLE assert result["data"] == entry_mock_data assert result["result"].unique_id == "000004d2" - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 200e2c21547..9a7f4a2bc50 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -381,6 +381,8 @@ async def test_import_discovery_integration( type=type_in_discovery, ), ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" assert result["title"] == TEST_NAME assert result["data"] == { @@ -395,7 +397,6 @@ async def test_import_discovery_integration( mock_save_json.assert_called_once() mock_remove.assert_not_called() - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -431,6 +432,7 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: assert result["step_id"] == "link" result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == TEST_NAME @@ -439,5 +441,4 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: CONF_TOKEN: TEST_TOKEN, } - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index 28e9db25363..9c02139d67c 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -108,7 +108,8 @@ async def test_form_already_configured( result["flow_id"], {"api_key": "test"}, ) + await hass.async_block_till_done() + assert result2["type"] == "abort" assert result2["reason"] == "already_configured" - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index 63e53627e0e..d69f9a93200 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -76,7 +76,8 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No CONF_HOST: "1.2.3.4", CONF_PORT: 1234, } - await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 @@ -102,8 +103,6 @@ async def test_user_duplicate( ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.usefixtures("filled_device_registry") diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index e1e7b01ac09..ae6172af6d8 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -119,10 +119,10 @@ async def test_success( context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT, ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] - await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1