diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index cdbea2ca346..159bfebc624 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod import asyncio import logging -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.storage import Store from .const import DOMAIN @@ -19,7 +19,7 @@ class AbstractConfig(ABC): _unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize abstract config.""" self.hass = hass self._store = None diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 4ba32c338b5..53bf44d8aa1 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -199,14 +199,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): # Don't migrate if there's a YAML config return - for state in self.hass.states.async_all(): - async_expose_entity( - self.hass, - CLOUD_ALEXA, - state.entity_id, - self._should_expose_legacy(state.entity_id), - ) - for entity_id in self._prefs.alexa_entity_configs: + for entity_id in { + *self.hass.states.async_entity_ids(), + *self._prefs.alexa_entity_configs, + }: async_expose_entity( self.hass, CLOUD_ALEXA, @@ -220,8 +216,18 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): async def on_hass_started(hass): if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION: - if self._prefs.alexa_settings_version < 2: + if self._prefs.alexa_settings_version < 2 or ( + # Recover from a bug we had in 2023.5.0 where entities didn't get exposed + self._prefs.alexa_settings_version < 3 + and not any( + settings.get("should_expose", False) + for settings in async_get_assistant_settings( + hass, CLOUD_ALEXA + ).values() + ) + ): self._migrate_alexa_entity_settings_v1() + await self._prefs.async_update( alexa_settings_version=ALEXA_SETTINGS_VERSION ) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 16848acc19d..351de5d0e65 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -12,6 +12,7 @@ from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.components.homeassistant.exposed_entities import ( async_expose_entity, + async_get_assistant_settings, async_get_entity_settings, async_listen_entity_updates, async_set_assistant_option, @@ -175,23 +176,10 @@ class CloudGoogleConfig(AbstractConfig): # Don't migrate if there's a YAML config return - for state in self.hass.states.async_all(): - entity_id = state.entity_id - async_expose_entity( - self.hass, - CLOUD_GOOGLE, - entity_id, - self._should_expose_legacy(entity_id), - ) - if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None): - async_set_assistant_option( - self.hass, - CLOUD_GOOGLE, - entity_id, - PREF_DISABLE_2FA, - _2fa_disabled, - ) - for entity_id in self._prefs.google_entity_configs: + for entity_id in { + *self.hass.states.async_entity_ids(), + *self._prefs.google_entity_configs, + }: async_expose_entity( self.hass, CLOUD_GOOGLE, @@ -213,8 +201,18 @@ class CloudGoogleConfig(AbstractConfig): async def on_hass_started(hass: HomeAssistant) -> None: if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION: - if self._prefs.google_settings_version < 2: + if self._prefs.google_settings_version < 2 or ( + # Recover from a bug we had in 2023.5.0 where entities didn't get exposed + self._prefs.google_settings_version < 3 + and not any( + settings.get("should_expose", False) + for settings in async_get_assistant_settings( + hass, CLOUD_GOOGLE + ).values() + ) + ): self._migrate_google_entity_settings_v1() + await self._prefs.async_update( google_settings_version=GOOGLE_SETTINGS_VERSION ) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 75e1856503c..5ccc007e524 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -41,8 +41,8 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 STORAGE_VERSION_MINOR = 2 -ALEXA_SETTINGS_VERSION = 2 -GOOGLE_SETTINGS_VERSION = 2 +ALEXA_SETTINGS_VERSION = 3 +GOOGLE_SETTINGS_VERSION = 3 class CloudPreferencesStore(Store): diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 26fab34f0e1..d7094a2e60b 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://www.home-assistant.io/integrations/elkm1", "iot_class": "local_push", "loggers": ["elkm1_lib"], - "requirements": ["elkm1-lib==2.2.1"] + "requirements": ["elkm1-lib==2.2.2"] } diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 61d6262250c..7ce195d68fc 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -25,6 +25,7 @@ from aioesphomeapi import ( NumberInfo, SelectInfo, SensorInfo, + SensorState, SwitchInfo, TextSensorInfo, UserService, @@ -240,9 +241,18 @@ class RuntimeEntryData: current_state_by_type = self.state[state_type] current_state = current_state_by_type.get(key, _SENTINEL) subscription_key = (state_type, key) - if current_state == state and subscription_key not in stale_state: + if ( + current_state == state + and subscription_key not in stale_state + and not ( + type(state) is SensorState # pylint: disable=unidiomatic-typecheck + and (platform_info := self.info.get(Platform.SENSOR)) + and (entity_info := platform_info.get(state.key)) + and (cast(SensorInfo, entity_info)).force_update + ) + ): _LOGGER.debug( - "%s: ignoring duplicate update with and key %s: %s", + "%s: ignoring duplicate update with key %s: %s", self.name, key, state, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 3576dadd1c0..ff78996f3aa 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==13.7.2", + "aioesphomeapi==13.7.3", "bluetooth-data-tools==0.4.0", "esphome-dashboard-api==1.2.3" ], diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index a5c507c3857..e6ac11889bc 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -1,6 +1,6 @@ """Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.""" import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import logging import aiohttp @@ -53,11 +53,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if result is False: return False - async def update_domain_callback(now): + async def update_domain_callback(now: datetime) -> None: """Update the FreeDNS entry.""" await _update_freedns(hass, session, url, auth_token) - async_track_time_interval(hass, update_domain_callback, update_interval) + async_track_time_interval( + hass, update_domain_callback, update_interval, cancel_on_shutdown=True + ) return True diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 78d974fe9cf..42a51c218b1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -590,7 +590,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await async_setup_addon_panel(hass, hassio) # Setup hardware integration for the detected board type - async def _async_setup_hardware_integration(hass): + async def _async_setup_hardware_integration(_: datetime) -> None: """Set up hardaware integration for the detected board type.""" if (os_info := get_os_info(hass)) is None: # os info not yet fetched from supervisor, retry later @@ -610,7 +610,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ) ) - await _async_setup_hardware_integration(hass) + await _async_setup_hardware_integration(datetime.now()) hass.async_create_task( hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 5ad8393726f..459f03edfbb 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -31,7 +31,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.helpers import discovery, event import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -208,16 +208,18 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 def _adapter_watchdog(now=None): _LOGGER.debug("Reached _adapter_watchdog") - event.call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + event.call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog_job) if not adapter.initialized: _LOGGER.info("Adapter not initialized; Trying to restart") hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) adapter.init() + _adapter_watchdog_job = HassJob(_adapter_watchdog, cancel_on_shutdown=True) + @callback def _async_initialized_callback(*_: Any): """Add watchdog on initialization.""" - return event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + return event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog_job) hdmi_network.set_initialized_callback(_async_initialized_callback) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 60d9a2d8504..0c1f4df6093 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -67,7 +67,7 @@ async def async_setup_platform( if isinstance(rest.last_exception, ssl.SSLError): _LOGGER.error( "Error connecting %s failed with %s", - conf[CONF_RESOURCE], + rest.url, rest.last_exception, ) return diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 8f1dd937391..95086f68d70 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -50,6 +50,11 @@ class RestData: self.last_exception: Exception | None = None self.headers: httpx.Headers | None = None + @property + def url(self) -> str: + """Get url.""" + return self._resource + def set_url(self, url: str) -> None: """Set url.""" self._resource = url diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index ead5a5893f4..6fc0b69d1fd 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -71,7 +71,7 @@ async def async_setup_platform( if isinstance(rest.last_exception, ssl.SSLError): _LOGGER.error( "Error connecting %s failed with %s", - conf[CONF_RESOURCE], + rest.url, rest.last_exception, ) return diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 9e016db0376..89b6529d483 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -6,8 +6,8 @@ from http import HTTPStatus import logging from typing import Any -import aiohttp import async_timeout +import httpx import voluptuous as vol from homeassistant.components.switch import ( @@ -30,8 +30,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.template_entity import ( TEMPLATE_ENTITY_BASE_SCHEMA, TemplateEntity, @@ -89,8 +89,8 @@ async def async_setup_platform( switch = RestSwitch(hass, config, unique_id) req = await switch.get_device_state(hass) - if req.status >= HTTPStatus.BAD_REQUEST: - _LOGGER.error("Got non-ok response from resource: %s", req.status) + if req.status_code >= HTTPStatus.BAD_REQUEST: + _LOGGER.error("Got non-ok response from resource: %s", req.status_code) else: async_add_entities([switch]) except (TypeError, ValueError): @@ -98,7 +98,7 @@ async def async_setup_platform( "Missing resource or schema in configuration. " "Add http:// or https:// to your URL" ) - except (asyncio.TimeoutError, aiohttp.ClientError) as exc: + except (asyncio.TimeoutError, httpx.RequestError) as exc: raise PlatformNotReady(f"No route to resource/endpoint: {resource}") from exc @@ -120,11 +120,11 @@ class RestSwitch(TemplateEntity, SwitchEntity): unique_id=unique_id, ) - auth: aiohttp.BasicAuth | None = None + auth: httpx.BasicAuth | None = None username: str | None = None if username := config.get(CONF_USERNAME): password: str = config[CONF_PASSWORD] - auth = aiohttp.BasicAuth(username, password=password) + auth = httpx.BasicAuth(username, password=password) self._resource: str = config[CONF_RESOURCE] self._state_resource: str = config.get(CONF_STATE_RESOURCE) or self._resource @@ -155,13 +155,13 @@ class RestSwitch(TemplateEntity, SwitchEntity): try: req = await self.set_device_state(body_on_t) - if req.status == HTTPStatus.OK: + if req.status_code == HTTPStatus.OK: self._attr_is_on = True else: _LOGGER.error( "Can't turn on %s. Is resource/endpoint offline?", self._resource ) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (asyncio.TimeoutError, httpx.RequestError): _LOGGER.error("Error while switching on %s", self._resource) async def async_turn_off(self, **kwargs: Any) -> None: @@ -170,24 +170,24 @@ class RestSwitch(TemplateEntity, SwitchEntity): try: req = await self.set_device_state(body_off_t) - if req.status == HTTPStatus.OK: + if req.status_code == HTTPStatus.OK: self._attr_is_on = False else: _LOGGER.error( "Can't turn off %s. Is resource/endpoint offline?", self._resource ) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (asyncio.TimeoutError, httpx.RequestError): _LOGGER.error("Error while switching off %s", self._resource) - async def set_device_state(self, body: Any) -> aiohttp.ClientResponse: + async def set_device_state(self, body: Any) -> httpx.Response: """Send a state update to the device.""" - websession = async_get_clientsession(self.hass, self._verify_ssl) + websession = get_async_client(self.hass, self._verify_ssl) rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): - req: aiohttp.ClientResponse = await getattr(websession, self._method)( + req: httpx.Response = await getattr(websession, self._method)( self._resource, auth=self._auth, data=bytes(body, "utf-8"), @@ -202,12 +202,12 @@ class RestSwitch(TemplateEntity, SwitchEntity): await self.get_device_state(self.hass) except asyncio.TimeoutError: _LOGGER.exception("Timed out while fetching data") - except aiohttp.ClientError as err: + except httpx.RequestError as err: _LOGGER.exception("Error while fetching data: %s", err) - async def get_device_state(self, hass: HomeAssistant) -> aiohttp.ClientResponse: + async def get_device_state(self, hass: HomeAssistant) -> httpx.Response: """Get the latest data from REST API and update the state.""" - websession = async_get_clientsession(hass, self._verify_ssl) + websession = get_async_client(hass, self._verify_ssl) rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) @@ -219,7 +219,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): headers=rendered_headers, params=rendered_params, ) - text = await req.text() + text = req.text if self._is_on_template is not None: text = self._is_on_template.async_render_with_possible_json_value( diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 6a86ce81445..ef2ecc7aa23 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -123,7 +123,7 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity): """ new_state = None if sia_event.code: - new_state = self.entity_description.code_consequences[sia_event.code] + new_state = self.entity_description.code_consequences.get(sia_event.code) if new_state is None: return False _LOGGER.debug("New state will be %s", new_state) diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py index 715fa26eee9..db0845473fd 100644 --- a/homeassistant/components/sia/binary_sensor.py +++ b/homeassistant/components/sia/binary_sensor.py @@ -132,7 +132,7 @@ class SIABinarySensor(SIABaseEntity, BinarySensorEntity): """ new_state = None if sia_event.code: - new_state = self.entity_description.code_consequences[sia_event.code] + new_state = self.entity_description.code_consequences.get(sia_event.code) if new_state is None: return False _LOGGER.debug("New state will be %s", new_state) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index c9c135fcccb..a5a8ed2f0d2 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["hatasmota==0.6.4"] + "requirements": ["hatasmota==0.6.5"] } diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index c620e264142..55db0f98244 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -12,6 +12,12 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, +) import homeassistant.util.dt as dt_util from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN @@ -23,12 +29,17 @@ ERROR_MULTIPLE_STATION = "Found multiple stations with the specified name" DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_FROM): cv.string, - vol.Required(CONF_TO): cv.string, - vol.Optional(CONF_TIME): cv.string, - vol.Required(CONF_WEEKDAY, default=WEEKDAYS): cv.multi_select( - {day: day for day in WEEKDAYS} + vol.Required(CONF_API_KEY): TextSelector(), + vol.Required(CONF_FROM): TextSelector(), + vol.Required(CONF_TO): TextSelector(), + vol.Optional(CONF_TIME): TextSelector(), + vol.Required(CONF_WEEKDAY, default=WEEKDAYS): SelectSelector( + SelectSelectorConfig( + options=WEEKDAYS, + multiple=True, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_WEEKDAY, + ) ), } ) diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json index 6f6ed44f7a5..6c67d881153 100644 --- a/homeassistant/components/trafikverket_train/strings.json +++ b/homeassistant/components/trafikverket_train/strings.json @@ -28,5 +28,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "mon": "Monday", + "tue": "Tuesday", + "wed": "Wednesday", + "thu": "Thursday", + "fri": "Friday", + "sat": "Saturday", + "sun": "Sunday" + } + } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 765755d1248..d8623e7bbe5 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -7,7 +7,11 @@ import logging from typing import Any import transmission_rpc -from transmission_rpc.error import TransmissionError +from transmission_rpc.error import ( + TransmissionAuthError, + TransmissionConnectError, + TransmissionError, +) import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -137,14 +141,13 @@ async def get_api(hass, entry): _LOGGER.debug("Successfully connected to %s", host) return api + except TransmissionAuthError as error: + _LOGGER.error("Credentials for Transmission client are not valid") + raise AuthenticationError from error + except TransmissionConnectError as error: + _LOGGER.error("Connecting to the Transmission client %s failed", host) + raise CannotConnect from error except TransmissionError as error: - if "401: Unauthorized" in str(error): - _LOGGER.error("Credentials for Transmission client are not valid") - raise AuthenticationError from error - if "111: Connection refused" in str(error): - _LOGGER.error("Connecting to the Transmission client %s failed", host) - raise CannotConnect from error - _LOGGER.error(error) raise UnknownError from error diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index d134c033ed7..53a3fb883ef 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -137,7 +137,19 @@ class Endpoint: ): cluster_handler_class = MultistateInput # end of ugly hack - cluster_handler = cluster_handler_class(cluster, self) + + try: + cluster_handler = cluster_handler_class(cluster, self) + except KeyError as err: + _LOGGER.warning( + "Cluster handler %s for cluster %s on endpoint %s is invalid: %s", + cluster_handler_class, + cluster, + self, + err, + ) + continue + if cluster_handler.name == const.CLUSTER_HANDLER_POWER_CONFIGURATION: self._device.power_configuration_ch = cluster_handler elif cluster_handler.name == const.CLUSTER_HANDLER_IDENTIFY: diff --git a/requirements_all.txt b/requirements_all.txt index 868a898a986..d863c93fc3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.7.2 +aioesphomeapi==13.7.3 # homeassistant.components.flo aioflo==2021.11.0 @@ -644,7 +644,7 @@ elgato==4.0.1 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==2.2.1 +elkm1-lib==2.2.2 # homeassistant.components.elmax elmax_api==0.0.4 @@ -881,7 +881,7 @@ hass_splunk==0.1.1 hassil==1.0.6 # homeassistant.components.tasmota -hatasmota==0.6.4 +hatasmota==0.6.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d57f935aa5..21f880490c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.7.2 +aioesphomeapi==13.7.3 # homeassistant.components.flo aioflo==2021.11.0 @@ -506,7 +506,7 @@ easyenergy==0.3.0 elgato==4.0.1 # homeassistant.components.elkm1 -elkm1-lib==2.2.1 +elkm1-lib==2.2.2 # homeassistant.components.elmax elmax_api==0.0.4 @@ -679,7 +679,7 @@ hass-nabucasa==0.66.2 hassil==1.0.6 # homeassistant.components.tasmota -hatasmota==0.6.4 +hatasmota==0.6.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 3a7e5a0874e..2be2a8eb2bb 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -542,11 +542,13 @@ async def test_alexa_handle_logout( assert len(mock_enable.return_value.mock_calls) == 1 +@pytest.mark.parametrize("alexa_settings_version", [1, 2]) async def test_alexa_config_migrate_expose_entity_prefs( hass: HomeAssistant, cloud_prefs: CloudPreferences, cloud_stub, entity_registry: er.EntityRegistry, + alexa_settings_version: int, ) -> None: """Test migrating Alexa entity config.""" hass.state = CoreState.starting @@ -593,7 +595,7 @@ async def test_alexa_config_migrate_expose_entity_prefs( await cloud_prefs.async_update( alexa_enabled=True, alexa_report_state=False, - alexa_settings_version=1, + alexa_settings_version=alexa_settings_version, ) expose_entity(hass, entity_migrated.entity_id, False) @@ -641,6 +643,100 @@ async def test_alexa_config_migrate_expose_entity_prefs( } +async def test_alexa_config_migrate_expose_entity_prefs_v2_no_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Alexa entity config from v2 to v3 when no entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + alexa_enabled=True, + alexa_report_state=False, + alexa_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, False) + + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = alexa_config.CloudAlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.alexa": {"should_expose": True} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.alexa": {"should_expose": True} + } + + +async def test_alexa_config_migrate_expose_entity_prefs_v2_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Alexa entity config from v2 to v3 when an entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + alexa_enabled=True, + alexa_report_state=False, + alexa_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, True) + + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = alexa_config.CloudAlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.alexa": {"should_expose": False} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.alexa": {"should_expose": True} + } + + async def test_alexa_config_migrate_expose_entity_prefs_default_none( hass: HomeAssistant, cloud_prefs: CloudPreferences, diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 45bc56a1700..fe60ca971a1 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -483,10 +483,12 @@ async def test_google_handle_logout( assert len(mock_enable.return_value.mock_calls) == 1 +@pytest.mark.parametrize("google_settings_version", [1, 2]) async def test_google_config_migrate_expose_entity_prefs( hass: HomeAssistant, cloud_prefs: CloudPreferences, entity_registry: er.EntityRegistry, + google_settings_version: int, ) -> None: """Test migrating Google entity config.""" hass.state = CoreState.starting @@ -540,7 +542,7 @@ async def test_google_config_migrate_expose_entity_prefs( await cloud_prefs.async_update( google_enabled=True, google_report_state=False, - google_settings_version=1, + google_settings_version=google_settings_version, ) expose_entity(hass, entity_migrated.entity_id, False) @@ -596,6 +598,100 @@ async def test_google_config_migrate_expose_entity_prefs( } +async def test_google_config_migrate_expose_entity_prefs_v2_no_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Google entity config from v2 to v3 when no entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + google_enabled=True, + google_report_state=False, + google_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, False) + + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.google_assistant": {"should_expose": True} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.google_assistant": {"should_expose": True} + } + + +async def test_google_config_migrate_expose_entity_prefs_v2_exposed( + hass: HomeAssistant, + cloud_prefs: CloudPreferences, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating Google entity config from v2 to v3 when an entity is exposed.""" + hass.state = CoreState.starting + + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set("light.state_only", "on") + entity_migrated = entity_registry.async_get_or_create( + "light", + "test", + "light_migrated", + suggested_object_id="migrated", + ) + await cloud_prefs.async_update( + google_enabled=True, + google_report_state=False, + google_settings_version=2, + ) + expose_entity(hass, "light.state_only", False) + expose_entity(hass, entity_migrated.entity_id, True) + + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = { + PREF_SHOULD_EXPOSE: True + } + cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_migrated.entity_id] = { + PREF_SHOULD_EXPOSE: True + } + conf = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False) + ) + await conf.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert async_get_entity_settings(hass, "light.state_only") == { + "cloud.google_assistant": {"should_expose": False} + } + assert async_get_entity_settings(hass, entity_migrated.entity_id) == { + "cloud.google_assistant": {"should_expose": True} + } + + async def test_google_config_migrate_expose_entity_prefs_default_none( hass: HomeAssistant, cloud_prefs: CloudPreferences, diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 655f172833b..5584fce5e3a 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -2,8 +2,9 @@ import asyncio from http import HTTPStatus -import aiohttp +import httpx import pytest +import respx from homeassistant.components.rest import DOMAIN from homeassistant.components.rest.switch import ( @@ -45,7 +46,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import assert_setup_component, async_fire_time_changed -from tests.test_util.aiohttp import AiohttpClientMocker NAME = "foo" DEVICE_CLASS = SwitchDeviceClass.SWITCH @@ -75,13 +75,13 @@ async def test_setup_missing_schema( assert "Invalid config for [switch.rest]: invalid url" in caplog.text +@respx.mock async def test_setup_failed_connect( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection error occurs.""" - aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() @@ -89,13 +89,13 @@ async def test_setup_failed_connect( assert "No route to resource/endpoint" in caplog.text +@respx.mock async def test_setup_timeout( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection timeout occurs.""" - aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() @@ -103,23 +103,21 @@ async def test_setup_timeout( assert "No route to resource/endpoint" in caplog.text -async def test_setup_minimum( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_setup_minimum(hass: HomeAssistant) -> None: """Test setup with minimum configuration.""" - aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + route = respx.get(RESOURCE) % HTTPStatus.OK config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} with assert_setup_component(1, SWITCH_DOMAIN): assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert aioclient_mock.call_count == 1 + assert route.call_count == 1 -async def test_setup_query_params( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_setup_query_params(hass: HomeAssistant) -> None: """Test setup with query params.""" - aioclient_mock.get("http://localhost/?search=something", status=HTTPStatus.OK) + route = respx.get("http://localhost/?search=something") % HTTPStatus.OK config = { SWITCH_DOMAIN: { CONF_PLATFORM: DOMAIN, @@ -131,12 +129,13 @@ async def test_setup_query_params( assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert aioclient_mock.call_count == 1 + assert route.call_count == 1 -async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: +@respx.mock +async def test_setup(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + route = respx.get(RESOURCE) % HTTPStatus.OK config = { SWITCH_DOMAIN: { CONF_PLATFORM: DOMAIN, @@ -149,16 +148,15 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) - } assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert aioclient_mock.call_count == 1 + assert route.call_count == 1 assert_setup_component(1, SWITCH_DOMAIN) -async def test_setup_with_state_resource( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_setup_with_state_resource(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - aioclient_mock.get(RESOURCE, status=HTTPStatus.NOT_FOUND) - aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK) + respx.get(RESOURCE) % HTTPStatus.NOT_FOUND + route = respx.get("http://localhost/state") % HTTPStatus.OK config = { SWITCH_DOMAIN: { CONF_PLATFORM: DOMAIN, @@ -172,15 +170,14 @@ async def test_setup_with_state_resource( } assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert aioclient_mock.call_count == 1 + assert route.call_count == 1 assert_setup_component(1, SWITCH_DOMAIN) -async def test_setup_with_templated_headers_params( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_setup_with_templated_headers_params(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + route = respx.get(RESOURCE) % HTTPStatus.OK config = { SWITCH_DOMAIN: { CONF_PLATFORM: DOMAIN, @@ -198,21 +195,21 @@ async def test_setup_with_templated_headers_params( } assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert aioclient_mock.call_count == 1 - assert aioclient_mock.mock_calls[-1][3].get("Accept") == CONTENT_TYPE_JSON - assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0" - assert aioclient_mock.mock_calls[-1][1].query["start"] == "0" - assert aioclient_mock.mock_calls[-1][1].query["end"] == "5" + assert route.call_count == 1 + last_call = route.calls[-1] + last_request: httpx.Request = last_call.request + assert last_request.headers.get("Accept") == CONTENT_TYPE_JSON + assert last_request.headers.get("User-Agent") == "Mozilla/5.0" + assert last_request.url.params["start"] == "0" + assert last_request.url.params["end"] == "5" assert_setup_component(1, SWITCH_DOMAIN) # Tests for REST switch platform. -async def _async_setup_test_switch( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) +async def _async_setup_test_switch(hass: HomeAssistant) -> None: + respx.get(RESOURCE) % HTTPStatus.OK headers = {"Content-type": CONTENT_TYPE_JSON} config = { @@ -223,51 +220,48 @@ async def _async_setup_test_switch( CONF_STATE_RESOURCE: STATE_RESOURCE, CONF_HEADERS: headers, } - assert await async_setup_component(hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: config}) await hass.async_block_till_done() assert_setup_component(1, SWITCH_DOMAIN) assert hass.states.get("switch.foo").state == STATE_UNKNOWN - aioclient_mock.clear_requests() + respx.reset() -async def test_name(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: +@respx.mock +async def test_name(hass: HomeAssistant) -> None: """Test the name.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) state = hass.states.get("switch.foo") assert state.attributes[ATTR_FRIENDLY_NAME] == NAME -async def test_device_class( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_device_class(hass: HomeAssistant) -> None: """Test the device class.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) state = hass.states.get("switch.foo") assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS -async def test_is_on_before_update( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_is_on_before_update(hass: HomeAssistant) -> None: """Test is_on in initial state.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) state = hass.states.get("switch.foo") assert state.state == STATE_UNKNOWN -async def test_turn_on_success( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_turn_on_success(hass: HomeAssistant) -> None: """Test turn_on.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) - aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + route = respx.post(RESOURCE) % HTTPStatus.OK + respx.get(RESOURCE).mock(side_effect=httpx.RequestError) assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -276,17 +270,18 @@ async def test_turn_on_success( ) await hass.async_block_till_done() - assert aioclient_mock.mock_calls[-2][2].decode() == "ON" + last_call = route.calls[-1] + last_request: httpx.Request = last_call.request + assert last_request.content.decode() == "ON" assert hass.states.get("switch.foo").state == STATE_ON -async def test_turn_on_status_not_ok( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_turn_on_status_not_ok(hass: HomeAssistant) -> None: """Test turn_on when error status returned.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) + route = respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -295,17 +290,18 @@ async def test_turn_on_status_not_ok( ) await hass.async_block_till_done() - assert aioclient_mock.mock_calls[-1][2].decode() == "ON" + last_call = route.calls[-1] + last_request: httpx.Request = last_call.request + assert last_request.content.decode() == "ON" assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_turn_on_timeout( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_turn_on_timeout(hass: HomeAssistant) -> None: """Test turn_on when timeout occurs.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) + respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -317,14 +313,13 @@ async def test_turn_on_timeout( assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_turn_off_success( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_turn_off_success(hass: HomeAssistant) -> None: """Test turn_off.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) - aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + route = respx.post(RESOURCE) % HTTPStatus.OK + respx.get(RESOURCE).mock(side_effect=httpx.RequestError) assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -333,18 +328,19 @@ async def test_turn_off_success( ) await hass.async_block_till_done() - assert aioclient_mock.mock_calls[-2][2].decode() == "OFF" + last_call = route.calls[-1] + last_request: httpx.Request = last_call.request + assert last_request.content.decode() == "OFF" assert hass.states.get("switch.foo").state == STATE_OFF -async def test_turn_off_status_not_ok( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_turn_off_status_not_ok(hass: HomeAssistant) -> None: """Test turn_off when error status returned.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) + route = respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -353,18 +349,19 @@ async def test_turn_off_status_not_ok( ) await hass.async_block_till_done() - assert aioclient_mock.mock_calls[-1][2].decode() == "OFF" + last_call = route.calls[-1] + last_request: httpx.Request = last_call.request + assert last_request.content.decode() == "OFF" assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_turn_off_timeout( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_turn_off_timeout(hass: HomeAssistant) -> None: """Test turn_off when timeout occurs.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.post(RESOURCE, exc=asyncio.TimeoutError()) + respx.post(RESOURCE).mock(side_effect=asyncio.TimeoutError()) assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -376,64 +373,59 @@ async def test_turn_off_timeout( assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_update_when_on( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_update_when_on(hass: HomeAssistant) -> None: """Test update when switch is on.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.get(RESOURCE, text="ON") + respx.get(RESOURCE).respond(text="ON") async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() assert hass.states.get("switch.foo").state == STATE_ON -async def test_update_when_off( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_update_when_off(hass: HomeAssistant) -> None: """Test update when switch is off.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.get(RESOURCE, text="OFF") + respx.get(RESOURCE).respond(text="OFF") async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() assert hass.states.get("switch.foo").state == STATE_OFF -async def test_update_when_unknown( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_update_when_unknown(hass: HomeAssistant) -> None: """Test update when unknown status returned.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.get(RESOURCE, text="unknown status") + respx.get(RESOURCE).respond(text="unknown status") async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_update_timeout( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_update_timeout(hass: HomeAssistant) -> None: """Test update when timeout occurs.""" - await _async_setup_test_switch(hass, aioclient_mock) + await _async_setup_test_switch(hass) - aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() assert hass.states.get("switch.foo").state == STATE_UNKNOWN -async def test_entity_config( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: +@respx.mock +async def test_entity_config(hass: HomeAssistant) -> None: """Test entity configuration.""" - aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + respx.get(RESOURCE) % HTTPStatus.OK config = { SWITCH_DOMAIN: { # REST configuration diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 7eee8fcbe7c..1d9334a2657 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -102,7 +102,7 @@ INDEXED_SENSOR_CONFIG_2 = { } -NESTED_SENSOR_CONFIG = { +NESTED_SENSOR_CONFIG_1 = { "sn": { "Time": "2020-03-03T00:00:00+00:00", "TX23": { @@ -119,6 +119,17 @@ NESTED_SENSOR_CONFIG = { } } +NESTED_SENSOR_CONFIG_2 = { + "sn": { + "Time": "2023-01-27T11:04:56", + "DS18B20": { + "Id": "01191ED79190", + "Temperature": 2.4, + }, + "TempUnit": "C", + } +} + async def test_controlling_state_via_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota @@ -174,12 +185,59 @@ async def test_controlling_state_via_mqtt( assert state.state == "20.0" +@pytest.mark.parametrize( + ("sensor_config", "entity_ids", "messages", "states"), + [ + ( + NESTED_SENSOR_CONFIG_1, + ["sensor.tasmota_tx23_speed_act", "sensor.tasmota_tx23_dir_card"], + ( + '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', + '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"},"Dir": {"Card": "ESE"}}}}', + ), + ( + { + "sensor.tasmota_tx23_speed_act": "12.3", + "sensor.tasmota_tx23_dir_card": "WSW", + }, + { + "sensor.tasmota_tx23_speed_act": "23.4", + "sensor.tasmota_tx23_dir_card": "ESE", + }, + ), + ), + ( + NESTED_SENSOR_CONFIG_2, + ["sensor.tasmota_ds18b20_temperature", "sensor.tasmota_ds18b20_id"], + ( + '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', + '{"StatusSNS":{"DS18B20":{"Id": "meep","Temperature": 23.4}}}', + ), + ( + { + "sensor.tasmota_ds18b20_temperature": "12.3", + "sensor.tasmota_ds18b20_id": "01191ED79190", + }, + { + "sensor.tasmota_ds18b20_temperature": "23.4", + "sensor.tasmota_ds18b20_id": "meep", + }, + ), + ), + ], +) async def test_nested_sensor_state_via_mqtt( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + setup_tasmota, + sensor_config, + entity_ids, + messages, + states, ) -> None: """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG) + sensor_config = copy.deepcopy(sensor_config) mac = config["mac"] async_fire_mqtt_message( @@ -195,31 +253,29 @@ async def test_nested_sensor_state_via_mqtt( ) await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) # Test periodic state update - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"TX23":{"Speed":{"Act":"12.3"}}}' - ) - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == "12.3" + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == states[0][entity_id] # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"}}}}', - ) - state = hass.states.get("sensor.tasmota_tx23_speed_act") - assert state.state == "23.4" + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == states[1][entity_id] async def test_indexed_sensor_state_via_mqtt( @@ -728,7 +784,7 @@ async def test_nested_sensor_attributes( ) -> None: """Test correct attributes for sensors.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG) + sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG_1) mac = config["mac"] async_fire_mqtt_message( @@ -754,7 +810,7 @@ async def test_nested_sensor_attributes( assert state.attributes.get("device_class") is None assert state.attributes.get("friendly_name") == "Tasmota TX23 Dir Avg" assert state.attributes.get("icon") is None - assert state.attributes.get("unit_of_measurement") == " " + assert state.attributes.get("unit_of_measurement") is None async def test_indexed_sensor_attributes( diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index d163708ce28..b4fae8e6f3d 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -2,7 +2,11 @@ from unittest.mock import MagicMock, patch import pytest -from transmission_rpc.error import TransmissionError +from transmission_rpc.error import ( + TransmissionAuthError, + TransmissionConnectError, + TransmissionError, +) from homeassistant import config_entries from homeassistant.components import transmission @@ -125,7 +129,7 @@ async def test_error_on_wrong_credentials( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_api.side_effect = TransmissionError("401: Unauthorized") + mock_api.side_effect = TransmissionAuthError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG_DATA, @@ -137,6 +141,21 @@ async def test_error_on_wrong_credentials( } +async def test_unexpected_error(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test we handle unexpected error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_api.side_effect = TransmissionError() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + async def test_error_on_connection_failure( hass: HomeAssistant, mock_api: MagicMock ) -> None: @@ -145,7 +164,7 @@ async def test_error_on_connection_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_api.side_effect = TransmissionError("111: Connection refused") + mock_api.side_effect = TransmissionConnectError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG_DATA, @@ -213,7 +232,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} - mock_api.side_effect = TransmissionError("401: Unauthorized") + mock_api.side_effect = TransmissionAuthError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -248,7 +267,7 @@ async def test_reauth_failed_connection_error( assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} - mock_api.side_effect = TransmissionError("111: Connection refused") + mock_api.side_effect = TransmissionConnectError() result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index da5e6859544..89ad0dd2410 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -3,7 +3,11 @@ from unittest.mock import MagicMock, patch import pytest -from transmission_rpc.error import TransmissionError +from transmission_rpc.error import ( + TransmissionAuthError, + TransmissionConnectError, + TransmissionError, +) from homeassistant.components.transmission.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -40,7 +44,7 @@ async def test_setup_failed_connection_error( entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - mock_api.side_effect = TransmissionError("111: Connection refused") + mock_api.side_effect = TransmissionConnectError() await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.SETUP_RETRY @@ -54,7 +58,21 @@ async def test_setup_failed_auth_error( entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - mock_api.side_effect = TransmissionError("401: Unauthorized") + mock_api.side_effect = TransmissionAuthError() + + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_setup_failed_unexpected_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test integration failed due to unexpected error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + mock_api.side_effect = TransmissionError() await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.SETUP_ERROR diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index c0c455542d3..1897383b6c4 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -1,11 +1,13 @@ """Test ZHA Core cluster handlers.""" import asyncio from collections.abc import Callable +import logging import math from unittest import mock from unittest.mock import AsyncMock, patch import pytest +import zigpy.device import zigpy.endpoint from zigpy.endpoint import Endpoint as ZigpyEndpoint import zigpy.profiles.zha @@ -791,3 +793,41 @@ async def test_configure_reporting(hass: HomeAssistant, endpoint) -> None: } ), ] + + +async def test_invalid_cluster_handler(hass: HomeAssistant, caplog) -> None: + """Test setting up a cluster handler that fails to match properly.""" + + class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler): + REPORT_CONFIG = ( + cluster_handlers.AttrReportConfig(attr="missing_attr", config=(1, 60, 1)), + ) + + mock_device = mock.AsyncMock(spec_set=zigpy.device.Device) + zigpy_ep = zigpy.endpoint.Endpoint(mock_device, endpoint_id=1) + + cluster = zigpy_ep.add_input_cluster(zigpy.zcl.clusters.lighting.Color.cluster_id) + cluster.configure_reporting_multiple = AsyncMock( + spec_set=cluster.configure_reporting_multiple, + return_value=[ + foundation.ConfigureReportingResponseRecord( + status=foundation.Status.SUCCESS + ) + ], + ) + + mock_zha_device = mock.AsyncMock(spec_set=ZHADevice) + zha_endpoint = Endpoint(zigpy_ep, mock_zha_device) + + # The cluster handler throws an error when matching this cluster + with pytest.raises(KeyError): + TestZigbeeClusterHandler(cluster, zha_endpoint) + + # And one is also logged at runtime + with patch.dict( + registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY, + {cluster.cluster_id: TestZigbeeClusterHandler}, + ), caplog.at_level(logging.WARNING): + zha_endpoint.add_all_cluster_handlers() + + assert "missing_attr" in caplog.text