From aea7a9af18340e3b5432d4b273200c122564615d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 20 Oct 2022 15:08:48 +0300 Subject: [PATCH] Bump aioshelly to 4.0.0 (#80423) * Bump aioshelly to 4.0.0 * Remove leftover * Fix number platform * Set last_update_success to false upon failure in number and climate * Set last_update_success upon failurie in entity --- homeassistant/components/shelly/__init__.py | 42 ++----- homeassistant/components/shelly/climate.py | 28 ++--- .../components/shelly/config_flow.py | 109 ++++++++---------- homeassistant/components/shelly/const.py | 6 - .../components/shelly/coordinator.py | 93 ++++++++------- homeassistant/components/shelly/entity.py | 41 +++---- homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/number.py | 24 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/test_config_flow.py | 88 ++++---------- 11 files changed, 173 insertions(+), 264 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 9759fd148d0..49a67b1a6a0 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -1,16 +1,12 @@ """The Shelly integration.""" from __future__ import annotations -import asyncio -from http import HTTPStatus from typing import Any, Final -from aiohttp import ClientResponseError import aioshelly from aioshelly.block_device import BlockDevice -from aioshelly.exceptions import AuthRequired, InvalidAuthError +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from aioshelly.rpc_device import RpcDevice -import async_timeout import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -23,7 +19,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( - AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_COAP_PORT, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, @@ -185,20 +180,11 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b # Not a sleeping device, finish setup LOGGER.debug("Setting up online block device %s", entry.title) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await device.initialize() - await device.update_status() - except asyncio.TimeoutError as err: - raise ConfigEntryNotReady( - str(err) or "Timeout during device setup" - ) from err - except OSError as err: - raise ConfigEntryNotReady(str(err) or "Error during device setup") from err - except AuthRequired as err: - raise ConfigEntryAuthFailed from err - except ClientResponseError as err: - if err.status == HTTPStatus.UNAUTHORIZED: - raise ConfigEntryAuthFailed from err + await device.initialize() + except DeviceConnectionError as err: + raise ConfigEntryNotReady(repr(err)) from err + except InvalidAuthError as err: + raise ConfigEntryAuthFailed(repr(err)) from err _async_block_device_setup() elif sleep_period is None or device_entry is None: @@ -283,16 +269,12 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo # Not a sleeping device, finish setup LOGGER.debug("Setting up online RPC device %s", entry.title) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await device.initialize() - except asyncio.TimeoutError as err: - raise ConfigEntryNotReady( - str(err) or "Timeout during device setup" - ) from err - except OSError as err: - raise ConfigEntryNotReady(str(err) or "Error during device setup") from err - except (AuthRequired, InvalidAuthError) as err: - raise ConfigEntryAuthFailed from err + await device.initialize() + except DeviceConnectionError as err: + raise ConfigEntryNotReady(repr(err)) from err + except InvalidAuthError as err: + raise ConfigEntryAuthFailed(repr(err)) from err + _async_rpc_device_setup() elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index a71cdd0b46d..38ba4a51c9f 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -1,13 +1,11 @@ """Climate support for Shelly.""" from __future__ import annotations -import asyncio from collections.abc import Mapping from typing import Any, cast from aioshelly.block_device import Block -from aioshelly.exceptions import AuthRequired -import async_timeout +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, @@ -20,13 +18,14 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, LOGGER, SHTRV_01_TEMPERATURE_SETTINGS +from .const import LOGGER, SHTRV_01_TEMPERATURE_SETTINGS from .coordinator import ShellyBlockCoordinator, get_entry_data from .utils import get_device_entry_gen @@ -238,19 +237,16 @@ class BlockSleepingClimate( """Set block state (HTTP request).""" LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await self.coordinator.device.http_request( - "get", f"thermostat/{self._channel}", kwargs - ) - except (asyncio.TimeoutError, OSError) as err: - LOGGER.error( - "Setting state for entity %s failed, state: %s, error: %s", - self.name, - kwargs, - repr(err), + return await self.coordinator.device.http_request( + "get", f"thermostat/{self._channel}", kwargs ) + except DeviceConnectionError as err: self.coordinator.last_update_success = False - return None + raise HomeAssistantError( + f"Setting state for entity {self.name} failed, state: {kwargs}, error: {repr(err)}" + ) from err + except InvalidAuthError: + self.coordinator.entry.async_start_reauth(self.hass) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -327,7 +323,7 @@ class BlockSleepingClimate( int(self.block.channel) ]["schedule_profile_names"], ] - except AuthRequired: + except InvalidAuthError: self.coordinator.entry.async_start_reauth(self.hass) else: self.async_write_ha_state() diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index baa9c218d5f..0f6ae9c9da6 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -1,16 +1,17 @@ """Config flow for Shelly integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping -from http import HTTPStatus from typing import Any, Final -import aiohttp import aioshelly from aioshelly.block_device import BlockDevice +from aioshelly.exceptions import ( + DeviceConnectionError, + FirmwareUnsupported, + InvalidAuthError, +) from aioshelly.rpc_device import RpcDevice -import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -20,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN, LOGGER +from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER from .utils import ( get_block_device_name, get_block_device_sleep_period, @@ -35,8 +36,6 @@ from .utils import ( HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) -HTTP_CONNECT_ERRORS: Final = (asyncio.TimeoutError, aiohttp.ClientError) - async def validate_input( hass: HomeAssistant, @@ -54,39 +53,38 @@ async def validate_input( data.get(CONF_PASSWORD), ) - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - if get_info_gen(info) == 2: - ws_context = await get_ws_context(hass) - rpc_device = await RpcDevice.create( - aiohttp_client.async_get_clientsession(hass), - ws_context, - options, - ) - await rpc_device.shutdown() - assert rpc_device.shelly - - return { - "title": get_rpc_device_name(rpc_device), - CONF_SLEEP_PERIOD: get_rpc_device_sleep_period(rpc_device.config), - "model": rpc_device.shelly.get("model"), - "gen": 2, - } - - # Gen1 - coap_context = await get_coap_context(hass) - block_device = await BlockDevice.create( + if get_info_gen(info) == 2: + ws_context = await get_ws_context(hass) + rpc_device = await RpcDevice.create( aiohttp_client.async_get_clientsession(hass), - coap_context, + ws_context, options, ) - block_device.shutdown() + await rpc_device.shutdown() + assert rpc_device.shelly + return { - "title": get_block_device_name(block_device), - CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), - "model": block_device.model, - "gen": 1, + "title": get_rpc_device_name(rpc_device), + CONF_SLEEP_PERIOD: get_rpc_device_sleep_period(rpc_device.config), + "model": rpc_device.shelly.get("model"), + "gen": 2, } + # Gen1 + coap_context = await get_coap_context(hass) + block_device = await BlockDevice.create( + aiohttp_client.async_get_clientsession(hass), + coap_context, + options, + ) + block_device.shutdown() + return { + "title": get_block_device_name(block_device), + CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), + "model": block_device.model, + "gen": 1, + } + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Shelly.""" @@ -107,9 +105,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host: str = user_input[CONF_HOST] try: self.info = await self._async_get_info(host) - except HTTP_CONNECT_ERRORS: + except DeviceConnectionError: errors["base"] = "cannot_connect" - except aioshelly.exceptions.FirmwareUnsupported: + except FirmwareUnsupported: return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") @@ -125,7 +123,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device_info = await validate_input( self.hass, self.host, self.info, {} ) - except HTTP_CONNECT_ERRORS: + except DeviceConnectionError: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") @@ -159,16 +157,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device_info = await validate_input( self.hass, self.host, self.info, user_input ) - except aiohttp.ClientResponseError as error: - if error.status == HTTPStatus.UNAUTHORIZED: - errors["base"] = "invalid_auth" - else: - errors["base"] = "cannot_connect" - except aioshelly.exceptions.InvalidAuthError: + except InvalidAuthError: errors["base"] = "invalid_auth" - except HTTP_CONNECT_ERRORS: - errors["base"] = "cannot_connect" - except aioshelly.exceptions.JSONRPCError: + except DeviceConnectionError: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") @@ -210,9 +201,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host = discovery_info.host try: self.info = await self._async_get_info(host) - except HTTP_CONNECT_ERRORS: + except DeviceConnectionError: return self.async_abort(reason="cannot_connect") - except aioshelly.exceptions.FirmwareUnsupported: + except FirmwareUnsupported: return self.async_abort(reason="unsupported_firmware") await self.async_set_unique_id(self.info["mac"]) @@ -231,7 +222,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.device_info = await validate_input(self.hass, self.host, self.info, {}) - except HTTP_CONNECT_ERRORS: + except DeviceConnectionError: return self.async_abort(reason="cannot_connect") return await self.async_step_confirm_discovery() @@ -284,23 +275,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await self._async_get_info(host) - except ( - asyncio.TimeoutError, - aiohttp.ClientError, - aioshelly.exceptions.FirmwareUnsupported, - ): + except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") if self.entry.data.get("gen", 1) != 1: user_input[CONF_USERNAME] = "admin" try: await validate_input(self.hass, host, info, user_input) - except ( - aiohttp.ClientResponseError, - aioshelly.exceptions.InvalidAuthError, - asyncio.TimeoutError, - aiohttp.ClientError, - ): + except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") else: self.hass.config_entries.async_update_entry( @@ -325,7 +307,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_info(self, host: str) -> dict[str, Any]: """Get info from shelly device.""" - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await aioshelly.common.get_info( - aiohttp_client.async_get_clientsession(self.hass), host - ) + return await aioshelly.common.get_info( + aiohttp_client.async_get_clientsession(self.hass), host + ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 401b8131487..39ca515e5ed 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -46,18 +46,12 @@ DUAL_MODE_LIGHT_MODELS: Final = ( "SHCB-1", ) -# Used in "_async_update_data" as timeout for polling data from devices. -POLLING_TIMEOUT_SEC: Final = 18 - # Refresh interval for REST sensors REST_SENSORS_UPDATE_INTERVAL: Final = 60 # Refresh interval for RPC polling sensors RPC_SENSORS_POLLING_INTERVAL: Final = 60 -# Timeout used for aioshelly calls -AIOSHELLY_DEVICE_TIMEOUT_SEC: Final = 10 - # Multiplier used to calculate the "update_interval" for sleeping devices. SLEEP_PERIOD_MULTIPLIER: Final = 1.2 CONF_SLEEP_PERIOD: Final = "sleep_period" diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index ec115cfc69f..f309fc99358 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -1,7 +1,6 @@ """Coordinators for the Shelly integration.""" from __future__ import annotations -import asyncio from collections.abc import Coroutine from dataclasses import dataclass from datetime import timedelta @@ -9,18 +8,18 @@ from typing import Any, cast import aioshelly from aioshelly.block_device import BlockDevice +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from aioshelly.rpc_device import RpcDevice -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - AIOSHELLY_DEVICE_TIMEOUT_SEC, ATTR_BETA, ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -36,7 +35,6 @@ from .const import ( INPUTS_EVENTS_DICT, LOGGER, MODELS_SUPPORTING_LIGHT_EFFECTS, - POLLING_TIMEOUT_SEC, REST_SENSORS_UPDATE_INTERVAL, RPC_INPUTS_EVENTS_TYPES, RPC_RECONNECT_INTERVAL, @@ -212,11 +210,13 @@ class ShellyBlockCoordinator(DataUpdateCoordinator): LOGGER.debug("Polling Shelly Block Device - %s", self.name) try: - async with async_timeout.timeout(POLLING_TIMEOUT_SEC): - await self.device.update() - device_update_info(self.hass, self.device, self.entry) - except OSError as err: - raise UpdateFailed("Error fetching data") from err + await self.device.update() + except DeviceConnectionError as err: + raise UpdateFailed(f"Error fetching data: {repr(err)}") from err + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) + else: + device_update_info(self.hass, self.device, self.entry) @property def model(self) -> str: @@ -278,11 +278,13 @@ class ShellyBlockCoordinator(DataUpdateCoordinator): new_version, ) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - result = await self.device.trigger_ota_update(beta=beta) - except (asyncio.TimeoutError, OSError) as err: - LOGGER.exception("Error while perform ota update: %s", err) - LOGGER.debug("Result of OTA update call: %s", result) + result = await self.device.trigger_ota_update(beta=beta) + except DeviceConnectionError as err: + raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) + else: + LOGGER.debug("Result of OTA update call: %s", result) def shutdown(self) -> None: """Shutdown the coordinator.""" @@ -323,20 +325,22 @@ class ShellyRestCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch data.""" + LOGGER.debug("REST update for %s", self.name) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - LOGGER.debug("REST update for %s", self.name) - await self.device.update_status() + await self.device.update_status() - if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: - return - old_firmware = self.device.firmware_version - await self.device.update_shelly() - if self.device.firmware_version == old_firmware: - return - device_update_info(self.hass, self.device, self.entry) - except OSError as err: - raise UpdateFailed("Error fetching data") from err + if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: + return + old_firmware = self.device.firmware_version + await self.device.update_shelly() + if self.device.firmware_version == old_firmware: + return + except DeviceConnectionError as err: + raise UpdateFailed(f"Error fetching data: {repr(err)}") from err + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) + else: + device_update_info(self.hass, self.device, self.entry) @property def mac(self) -> str: @@ -436,13 +440,14 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): if self.device.connected: return + LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) try: - LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await self.device.initialize() - device_update_info(self.hass, self.device, self.entry) - except OSError as err: - raise UpdateFailed("Device disconnected") from err + await self.device.initialize() + device_update_info(self.hass, self.device, self.entry) + except DeviceConnectionError as err: + raise UpdateFailed(f"Device disconnected: {repr(err)}") from err + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) @property def model(self) -> str: @@ -503,12 +508,13 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): new_version, ) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await self.device.trigger_ota_update(beta=beta) - except (asyncio.TimeoutError, OSError) as err: - LOGGER.exception("Error while perform ota update: %s", err) - - LOGGER.debug("OTA update call successful") + await self.device.trigger_ota_update(beta=beta) + except DeviceConnectionError as err: + raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) + else: + LOGGER.debug("OTA update call successful") async def shutdown(self) -> None: """Shutdown the coordinator.""" @@ -544,12 +550,13 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator): if not self.device.connected: raise UpdateFailed("Device disconnected") + LOGGER.debug("Polling Shelly RPC Device - %s", self.name) try: - LOGGER.debug("Polling Shelly RPC Device - %s", self.name) - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await self.device.update_status() - except (OSError, aioshelly.exceptions.RPCTimeout) as err: - raise UpdateFailed("Device disconnected") from err + await self.device.update_status() + except DeviceConnectionError as err: + raise UpdateFailed(f"Device disconnected: {repr(err)}") from err + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) @property def model(self) -> str: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 27f7b5dc689..72794be60cc 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -1,16 +1,16 @@ """Shelly entity helper.""" from __future__ import annotations -import asyncio from collections.abc import Callable, Mapping from dataclasses import dataclass from typing import Any, cast from aioshelly.block_device import Block -import async_timeout +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry, entity, entity_registry from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, LOGGER +from .const import CONF_SLEEP_PERIOD, LOGGER from .coordinator import ( ShellyBlockCoordinator, ShellyRpcCoordinator, @@ -362,17 +362,14 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Set block state (HTTP request).""" LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await self.block.set_state(**kwargs) - except (asyncio.TimeoutError, OSError) as err: - LOGGER.error( - "Setting state for entity %s failed, state: %s, error: %s", - self.name, - kwargs, - repr(err), - ) + return await self.block.set_state(**kwargs) + except DeviceConnectionError as err: self.coordinator.last_update_success = False - return None + raise HomeAssistantError( + f"Setting state for entity {self.name} failed, state: {kwargs}, error: {repr(err)}" + ) from err + except InvalidAuthError: + self.coordinator.entry.async_start_reauth(self.hass) class ShellyRpcEntity(entity.Entity): @@ -425,18 +422,14 @@ class ShellyRpcEntity(entity.Entity): params, ) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await self.coordinator.device.call_rpc(method, params) - except asyncio.TimeoutError as err: - LOGGER.error( - "Call RPC for entity %s failed, method: %s, params: %s, error: %s", - self.name, - method, - params, - repr(err), - ) + return await self.coordinator.device.call_rpc(method, params) + except DeviceConnectionError as err: self.coordinator.last_update_success = False - return None + raise HomeAssistantError( + f"Call RPC for entity {self.name} failed, method: {method}, params: {params}, error: {repr(err)}" + ) from err + except InvalidAuthError: + self.coordinator.entry.async_start_reauth(self.hass) class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 6996a42e022..b3bd329b868 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==3.0.0"], + "requirements": ["aioshelly==4.0.0"], "dependencies": ["http"], "zeroconf": [ { diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index eb61fccb9ef..bb7f17ea18d 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -1,11 +1,10 @@ """Number for Shelly.""" from __future__ import annotations -import asyncio from dataclasses import dataclass from typing import Any, Final, cast -import async_timeout +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from homeassistant.components.number import ( NumberEntity, @@ -15,11 +14,12 @@ from homeassistant.components.number import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, LOGGER +from .const import CONF_SLEEP_PERIOD, LOGGER from .entity import ( BlockEntityDescription, ShellySleepingBlockAttributeEntity, @@ -115,15 +115,13 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity): async def _set_state_full_path(self, path: str, params: Any) -> Any: """Set block state (HTTP request).""" - LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await self.coordinator.device.http_request("get", path, params) - except (asyncio.TimeoutError, OSError) as err: - LOGGER.error( - "Setting state for entity %s failed, state: %s, error: %s", - self.name, - params, - repr(err), - ) + return await self.coordinator.device.http_request("get", path, params) + except DeviceConnectionError as err: + self.coordinator.last_update_success = False + raise HomeAssistantError( + f"Setting state for entity {self.name} failed, state: {params}, error: {repr(err)}" + ) from err + except InvalidAuthError: + self.coordinator.entry.async_start_reauth(self.hass) diff --git a/requirements_all.txt b/requirements_all.txt index 4826d0003b0..90b961b0961 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==3.0.0 +aioshelly==4.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43f57742039..f2808df8629 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==3.0.0 +aioshelly==4.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index a761fe7836e..ad28ffbd4f0 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Shelly config flow.""" -import asyncio -from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch -import aiohttp -import aioshelly +from aioshelly.exceptions import ( + DeviceConnectionError, + FirmwareUnsupported, + InvalidAuthError, +) import pytest from homeassistant import config_entries, data_entry_flow @@ -207,7 +208,7 @@ async def test_form_auth(hass, test_data): @pytest.mark.parametrize( - "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] + "error", [(DeviceConnectionError, "cannot_connect"), (ValueError, "unknown")] ) async def test_form_errors_get_info(hass, error): """Test we handle errors.""" @@ -324,7 +325,7 @@ async def test_form_missing_model_key_zeroconf(hass, caplog): @pytest.mark.parametrize( - "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] + "error", [(DeviceConnectionError, "cannot_connect"), (ValueError, "unknown")] ) async def test_form_errors_test_connection(hass, error): """Test we handle errors.""" @@ -431,10 +432,7 @@ async def test_form_firmware_unsupported(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "aioshelly.common.get_info", - side_effect=aioshelly.exceptions.FirmwareUnsupported, - ): + with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -447,15 +445,8 @@ async def test_form_firmware_unsupported(hass): @pytest.mark.parametrize( "error", [ - ( - aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST), - "cannot_connect", - ), - ( - aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED), - "invalid_auth", - ), - (asyncio.TimeoutError, "cannot_connect"), + (InvalidAuthError, "invalid_auth"), + (DeviceConnectionError, "cannot_connect"), (ValueError, "unknown"), ], ) @@ -490,15 +481,8 @@ async def test_form_auth_errors_test_connection_gen1(hass, error): @pytest.mark.parametrize( "error", [ - ( - aioshelly.exceptions.JSONRPCError(code=400), - "cannot_connect", - ), - ( - aioshelly.exceptions.InvalidAuthError(code=401), - "invalid_auth", - ), - (asyncio.TimeoutError, "cannot_connect"), + (DeviceConnectionError, "cannot_connect"), + (InvalidAuthError, "invalid_auth"), (ValueError, "unknown"), ], ) @@ -647,20 +631,8 @@ async def test_zeroconf_sleeping_device(hass): assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize( - "error", - [ - ( - aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST), - "cannot_connect", - ), - (asyncio.TimeoutError, "cannot_connect"), - ], -) -async def test_zeroconf_sleeping_device_error(hass, error): +async def test_zeroconf_sleeping_device_error(hass): """Test sleeping device configuration via zeroconf with error.""" - exc = error - with patch( "aioshelly.common.get_info", return_value={ @@ -671,7 +643,7 @@ async def test_zeroconf_sleeping_device_error(hass, error): }, ), patch( "aioshelly.block_device.BlockDevice.create", - new=AsyncMock(side_effect=exc), + new=AsyncMock(side_effect=DeviceConnectionError), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -708,10 +680,7 @@ async def test_zeroconf_already_configured(hass): async def test_zeroconf_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" - with patch( - "aioshelly.common.get_info", - side_effect=aioshelly.exceptions.FirmwareUnsupported, - ): + with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, @@ -724,7 +693,7 @@ async def test_zeroconf_firmware_unsupported(hass): async def test_zeroconf_cannot_connect(hass): """Test we get the form.""" - with patch("aioshelly.common.get_info", side_effect=asyncio.TimeoutError): + with patch("aioshelly.common.get_info", side_effect=DeviceConnectionError): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, @@ -840,21 +809,13 @@ async def test_reauth_successful(hass, test_data): @pytest.mark.parametrize( "test_data", [ - ( - 1, - {"username": "test user", "password": "test1 password"}, - aioshelly.exceptions.InvalidAuthError(code=HTTPStatus.UNAUTHORIZED.value), - ), - ( - 2, - {"password": "test2 password"}, - aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED), - ), + (1, {"username": "test user", "password": "test1 password"}), + (2, {"password": "test2 password"}), ], ) async def test_reauth_unsuccessful(hass, test_data): """Test reauthentication flow failed.""" - gen, user_input, exc = test_data + gen, user_input = test_data entry = MockConfigEntry( domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": gen} ) @@ -865,9 +826,10 @@ async def test_reauth_unsuccessful(hass, test_data): return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, ), patch( "aioshelly.block_device.BlockDevice.create", - new=AsyncMock(side_effect=exc), + new=AsyncMock(side_effect=InvalidAuthError), ), patch( - "aioshelly.rpc_device.RpcDevice.create", new=AsyncMock(side_effect=exc) + "aioshelly.rpc_device.RpcDevice.create", + new=AsyncMock(side_effect=InvalidAuthError), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -889,11 +851,7 @@ async def test_reauth_unsuccessful(hass, test_data): @pytest.mark.parametrize( "error", - [ - asyncio.TimeoutError, - aiohttp.ClientError, - aioshelly.exceptions.FirmwareUnsupported, - ], + [DeviceConnectionError, FirmwareUnsupported], ) async def test_reauth_get_info_error(hass, error): """Test reauthentication flow failed with error in get_info()."""