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
This commit is contained in:
Shay Levy 2022-10-20 15:08:48 +03:00 committed by GitHub
parent 2c43606922
commit aea7a9af18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 173 additions and 264 deletions

View File

@ -1,16 +1,12 @@
"""The Shelly integration.""" """The Shelly integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from http import HTTPStatus
from typing import Any, Final from typing import Any, Final
from aiohttp import ClientResponseError
import aioshelly import aioshelly
from aioshelly.block_device import BlockDevice from aioshelly.block_device import BlockDevice
from aioshelly.exceptions import AuthRequired, InvalidAuthError from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
from aioshelly.rpc_device import RpcDevice from aioshelly.rpc_device import RpcDevice
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry 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 homeassistant.util.unit_system import METRIC_SYSTEM
from .const import ( from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC,
CONF_COAP_PORT, CONF_COAP_PORT,
CONF_SLEEP_PERIOD, CONF_SLEEP_PERIOD,
DATA_CONFIG_ENTRY, DATA_CONFIG_ENTRY,
@ -185,20 +180,11 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
# Not a sleeping device, finish setup # Not a sleeping device, finish setup
LOGGER.debug("Setting up online block device %s", entry.title) LOGGER.debug("Setting up online block device %s", entry.title)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
await device.initialize() await device.initialize()
await device.update_status() except DeviceConnectionError as err:
except asyncio.TimeoutError as err: raise ConfigEntryNotReady(repr(err)) from err
raise ConfigEntryNotReady( except InvalidAuthError as err:
str(err) or "Timeout during device setup" raise ConfigEntryAuthFailed(repr(err)) from err
) 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
_async_block_device_setup() _async_block_device_setup()
elif sleep_period is None or device_entry is None: 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 # Not a sleeping device, finish setup
LOGGER.debug("Setting up online RPC device %s", entry.title) LOGGER.debug("Setting up online RPC device %s", entry.title)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
await device.initialize() await device.initialize()
except asyncio.TimeoutError as err: except DeviceConnectionError as err:
raise ConfigEntryNotReady( raise ConfigEntryNotReady(repr(err)) from err
str(err) or "Timeout during device setup" except InvalidAuthError as err:
) from err raise ConfigEntryAuthFailed(repr(err)) 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
_async_rpc_device_setup() _async_rpc_device_setup()
elif sleep_period is None or device_entry is None: elif sleep_period is None or device_entry is None:
# Need to get sleep info or first time sleeping device setup, wait for device # Need to get sleep info or first time sleeping device setup, wait for device

View File

@ -1,13 +1,11 @@
"""Climate support for Shelly.""" """Climate support for Shelly."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any, cast from typing import Any, cast
from aioshelly.block_device import Block from aioshelly.block_device import Block
from aioshelly.exceptions import AuthRequired from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
import async_timeout
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
@ -20,13 +18,14 @@ from homeassistant.components.climate import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant, State, callback from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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 .coordinator import ShellyBlockCoordinator, get_entry_data
from .utils import get_device_entry_gen from .utils import get_device_entry_gen
@ -238,19 +237,16 @@ class BlockSleepingClimate(
"""Set block state (HTTP request).""" """Set block state (HTTP request)."""
LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.coordinator.device.http_request( return await self.coordinator.device.http_request(
"get", f"thermostat/{self._channel}", kwargs "get", f"thermostat/{self._channel}", kwargs
) )
except (asyncio.TimeoutError, OSError) as err: except DeviceConnectionError as err:
LOGGER.error(
"Setting state for entity %s failed, state: %s, error: %s",
self.name,
kwargs,
repr(err),
)
self.coordinator.last_update_success = False 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: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
@ -327,7 +323,7 @@ class BlockSleepingClimate(
int(self.block.channel) int(self.block.channel)
]["schedule_profile_names"], ]["schedule_profile_names"],
] ]
except AuthRequired: except InvalidAuthError:
self.coordinator.entry.async_start_reauth(self.hass) self.coordinator.entry.async_start_reauth(self.hass)
else: else:
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -1,16 +1,17 @@
"""Config flow for Shelly integration.""" """Config flow for Shelly integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Mapping from collections.abc import Mapping
from http import HTTPStatus
from typing import Any, Final from typing import Any, Final
import aiohttp
import aioshelly import aioshelly
from aioshelly.block_device import BlockDevice from aioshelly.block_device import BlockDevice
from aioshelly.exceptions import (
DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
)
from aioshelly.rpc_device import RpcDevice from aioshelly.rpc_device import RpcDevice
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -20,7 +21,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client 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 ( from .utils import (
get_block_device_name, get_block_device_name,
get_block_device_sleep_period, get_block_device_sleep_period,
@ -35,8 +36,6 @@ from .utils import (
HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str})
HTTP_CONNECT_ERRORS: Final = (asyncio.TimeoutError, aiohttp.ClientError)
async def validate_input( async def validate_input(
hass: HomeAssistant, hass: HomeAssistant,
@ -54,7 +53,6 @@ async def validate_input(
data.get(CONF_PASSWORD), data.get(CONF_PASSWORD),
) )
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
if get_info_gen(info) == 2: if get_info_gen(info) == 2:
ws_context = await get_ws_context(hass) ws_context = await get_ws_context(hass)
rpc_device = await RpcDevice.create( rpc_device = await RpcDevice.create(
@ -107,9 +105,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
host: str = user_input[CONF_HOST] host: str = user_input[CONF_HOST]
try: try:
self.info = await self._async_get_info(host) self.info = await self._async_get_info(host)
except HTTP_CONNECT_ERRORS: except DeviceConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except aioshelly.exceptions.FirmwareUnsupported: except FirmwareUnsupported:
return self.async_abort(reason="unsupported_firmware") return self.async_abort(reason="unsupported_firmware")
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception") LOGGER.exception("Unexpected exception")
@ -125,7 +123,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
device_info = await validate_input( device_info = await validate_input(
self.hass, self.host, self.info, {} self.hass, self.host, self.info, {}
) )
except HTTP_CONNECT_ERRORS: except DeviceConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception") LOGGER.exception("Unexpected exception")
@ -159,16 +157,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
device_info = await validate_input( device_info = await validate_input(
self.hass, self.host, self.info, user_input self.hass, self.host, self.info, user_input
) )
except aiohttp.ClientResponseError as error: except InvalidAuthError:
if error.status == HTTPStatus.UNAUTHORIZED:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
else: except DeviceConnectionError:
errors["base"] = "cannot_connect"
except aioshelly.exceptions.InvalidAuthError:
errors["base"] = "invalid_auth"
except HTTP_CONNECT_ERRORS:
errors["base"] = "cannot_connect"
except aioshelly.exceptions.JSONRPCError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception") LOGGER.exception("Unexpected exception")
@ -210,9 +201,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
host = discovery_info.host host = discovery_info.host
try: try:
self.info = await self._async_get_info(host) self.info = await self._async_get_info(host)
except HTTP_CONNECT_ERRORS: except DeviceConnectionError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except aioshelly.exceptions.FirmwareUnsupported: except FirmwareUnsupported:
return self.async_abort(reason="unsupported_firmware") return self.async_abort(reason="unsupported_firmware")
await self.async_set_unique_id(self.info["mac"]) await self.async_set_unique_id(self.info["mac"])
@ -231,7 +222,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
self.device_info = await validate_input(self.hass, self.host, self.info, {}) 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 self.async_abort(reason="cannot_connect")
return await self.async_step_confirm_discovery() return await self.async_step_confirm_discovery()
@ -284,23 +275,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
info = await self._async_get_info(host) info = await self._async_get_info(host)
except ( except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported):
asyncio.TimeoutError,
aiohttp.ClientError,
aioshelly.exceptions.FirmwareUnsupported,
):
return self.async_abort(reason="reauth_unsuccessful") return self.async_abort(reason="reauth_unsuccessful")
if self.entry.data.get("gen", 1) != 1: if self.entry.data.get("gen", 1) != 1:
user_input[CONF_USERNAME] = "admin" user_input[CONF_USERNAME] = "admin"
try: try:
await validate_input(self.hass, host, info, user_input) await validate_input(self.hass, host, info, user_input)
except ( except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported):
aiohttp.ClientResponseError,
aioshelly.exceptions.InvalidAuthError,
asyncio.TimeoutError,
aiohttp.ClientError,
):
return self.async_abort(reason="reauth_unsuccessful") return self.async_abort(reason="reauth_unsuccessful")
else: else:
self.hass.config_entries.async_update_entry( 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]: async def _async_get_info(self, host: str) -> dict[str, Any]:
"""Get info from shelly device.""" """Get info from shelly device."""
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await aioshelly.common.get_info( return await aioshelly.common.get_info(
aiohttp_client.async_get_clientsession(self.hass), host aiohttp_client.async_get_clientsession(self.hass), host
) )

View File

@ -46,18 +46,12 @@ DUAL_MODE_LIGHT_MODELS: Final = (
"SHCB-1", "SHCB-1",
) )
# Used in "_async_update_data" as timeout for polling data from devices.
POLLING_TIMEOUT_SEC: Final = 18
# Refresh interval for REST sensors # Refresh interval for REST sensors
REST_SENSORS_UPDATE_INTERVAL: Final = 60 REST_SENSORS_UPDATE_INTERVAL: Final = 60
# Refresh interval for RPC polling sensors # Refresh interval for RPC polling sensors
RPC_SENSORS_POLLING_INTERVAL: Final = 60 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. # Multiplier used to calculate the "update_interval" for sleeping devices.
SLEEP_PERIOD_MULTIPLIER: Final = 1.2 SLEEP_PERIOD_MULTIPLIER: Final = 1.2
CONF_SLEEP_PERIOD: Final = "sleep_period" CONF_SLEEP_PERIOD: Final = "sleep_period"

View File

@ -1,7 +1,6 @@
"""Coordinators for the Shelly integration.""" """Coordinators for the Shelly integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Coroutine from collections.abc import Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
@ -9,18 +8,18 @@ from typing import Any, cast
import aioshelly import aioshelly
from aioshelly.block_device import BlockDevice from aioshelly.block_device import BlockDevice
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
from aioshelly.rpc_device import RpcDevice from aioshelly.rpc_device import RpcDevice
import async_timeout
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry from homeassistant.helpers import device_registry
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC,
ATTR_BETA, ATTR_BETA,
ATTR_CHANNEL, ATTR_CHANNEL,
ATTR_CLICK_TYPE, ATTR_CLICK_TYPE,
@ -36,7 +35,6 @@ from .const import (
INPUTS_EVENTS_DICT, INPUTS_EVENTS_DICT,
LOGGER, LOGGER,
MODELS_SUPPORTING_LIGHT_EFFECTS, MODELS_SUPPORTING_LIGHT_EFFECTS,
POLLING_TIMEOUT_SEC,
REST_SENSORS_UPDATE_INTERVAL, REST_SENSORS_UPDATE_INTERVAL,
RPC_INPUTS_EVENTS_TYPES, RPC_INPUTS_EVENTS_TYPES,
RPC_RECONNECT_INTERVAL, RPC_RECONNECT_INTERVAL,
@ -212,11 +210,13 @@ class ShellyBlockCoordinator(DataUpdateCoordinator):
LOGGER.debug("Polling Shelly Block Device - %s", self.name) LOGGER.debug("Polling Shelly Block Device - %s", self.name)
try: try:
async with async_timeout.timeout(POLLING_TIMEOUT_SEC):
await self.device.update() 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) device_update_info(self.hass, self.device, self.entry)
except OSError as err:
raise UpdateFailed("Error fetching data") from err
@property @property
def model(self) -> str: def model(self) -> str:
@ -278,10 +278,12 @@ class ShellyBlockCoordinator(DataUpdateCoordinator):
new_version, new_version,
) )
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
result = await self.device.trigger_ota_update(beta=beta) result = await self.device.trigger_ota_update(beta=beta)
except (asyncio.TimeoutError, OSError) as err: except DeviceConnectionError as err:
LOGGER.exception("Error while perform ota update: %s", 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) LOGGER.debug("Result of OTA update call: %s", result)
def shutdown(self) -> None: def shutdown(self) -> None:
@ -323,9 +325,8 @@ class ShellyRestCoordinator(DataUpdateCoordinator):
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
"""Fetch data.""" """Fetch data."""
try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
LOGGER.debug("REST update for %s", self.name) LOGGER.debug("REST update for %s", self.name)
try:
await self.device.update_status() await self.device.update_status()
if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL:
@ -334,9 +335,12 @@ class ShellyRestCoordinator(DataUpdateCoordinator):
await self.device.update_shelly() await self.device.update_shelly()
if self.device.firmware_version == old_firmware: if self.device.firmware_version == old_firmware:
return 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) device_update_info(self.hass, self.device, self.entry)
except OSError as err:
raise UpdateFailed("Error fetching data") from err
@property @property
def mac(self) -> str: def mac(self) -> str:
@ -436,13 +440,14 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
if self.device.connected: if self.device.connected:
return return
try:
LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name)
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): try:
await self.device.initialize() await self.device.initialize()
device_update_info(self.hass, self.device, self.entry) device_update_info(self.hass, self.device, self.entry)
except OSError as err: except DeviceConnectionError as err:
raise UpdateFailed("Device disconnected") from err raise UpdateFailed(f"Device disconnected: {repr(err)}") from err
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
@property @property
def model(self) -> str: def model(self) -> str:
@ -503,11 +508,12 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
new_version, new_version,
) )
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
await self.device.trigger_ota_update(beta=beta) await self.device.trigger_ota_update(beta=beta)
except (asyncio.TimeoutError, OSError) as err: except DeviceConnectionError as err:
LOGGER.exception("Error while perform ota update: %s", 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") LOGGER.debug("OTA update call successful")
async def shutdown(self) -> None: async def shutdown(self) -> None:
@ -544,12 +550,13 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator):
if not self.device.connected: if not self.device.connected:
raise UpdateFailed("Device disconnected") raise UpdateFailed("Device disconnected")
try:
LOGGER.debug("Polling Shelly RPC Device - %s", self.name) LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): try:
await self.device.update_status() await self.device.update_status()
except (OSError, aioshelly.exceptions.RPCTimeout) as err: except DeviceConnectionError as err:
raise UpdateFailed("Device disconnected") from err raise UpdateFailed(f"Device disconnected: {repr(err)}") from err
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
@property @property
def model(self) -> str: def model(self) -> str:

View File

@ -1,16 +1,16 @@
"""Shelly entity helper.""" """Shelly entity helper."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, cast from typing import Any, cast
from aioshelly.block_device import Block from aioshelly.block_device import Block
import async_timeout from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry, entity, entity_registry from homeassistant.helpers import device_registry, entity, entity_registry
from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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 ( from .coordinator import (
ShellyBlockCoordinator, ShellyBlockCoordinator,
ShellyRpcCoordinator, ShellyRpcCoordinator,
@ -362,17 +362,14 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
"""Set block state (HTTP request).""" """Set block state (HTTP request)."""
LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.block.set_state(**kwargs) return await self.block.set_state(**kwargs)
except (asyncio.TimeoutError, OSError) as err: except DeviceConnectionError as err:
LOGGER.error(
"Setting state for entity %s failed, state: %s, error: %s",
self.name,
kwargs,
repr(err),
)
self.coordinator.last_update_success = False 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): class ShellyRpcEntity(entity.Entity):
@ -425,18 +422,14 @@ class ShellyRpcEntity(entity.Entity):
params, params,
) )
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.coordinator.device.call_rpc(method, params) return await self.coordinator.device.call_rpc(method, params)
except asyncio.TimeoutError as err: except DeviceConnectionError as err:
LOGGER.error(
"Call RPC for entity %s failed, method: %s, params: %s, error: %s",
self.name,
method,
params,
repr(err),
)
self.coordinator.last_update_success = False 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): class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):

View File

@ -3,7 +3,7 @@
"name": "Shelly", "name": "Shelly",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly", "documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==3.0.0"], "requirements": ["aioshelly==4.0.0"],
"dependencies": ["http"], "dependencies": ["http"],
"zeroconf": [ "zeroconf": [
{ {

View File

@ -1,11 +1,10 @@
"""Number for Shelly.""" """Number for Shelly."""
from __future__ import annotations from __future__ import annotations
import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Final, cast from typing import Any, Final, cast
import async_timeout from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
from homeassistant.components.number import ( from homeassistant.components.number import (
NumberEntity, NumberEntity,
@ -15,11 +14,12 @@ from homeassistant.components.number import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry 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 ( from .entity import (
BlockEntityDescription, BlockEntityDescription,
ShellySleepingBlockAttributeEntity, ShellySleepingBlockAttributeEntity,
@ -115,15 +115,13 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity):
async def _set_state_full_path(self, path: str, params: Any) -> Any: async def _set_state_full_path(self, path: str, params: Any) -> Any:
"""Set block state (HTTP request).""" """Set block state (HTTP request)."""
LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) LOGGER.debug("Setting state for entity %s, state: %s", self.name, params)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await self.coordinator.device.http_request("get", path, params) return await self.coordinator.device.http_request("get", path, params)
except (asyncio.TimeoutError, OSError) as err: except DeviceConnectionError as err:
LOGGER.error( self.coordinator.last_update_success = False
"Setting state for entity %s failed, state: %s, error: %s", raise HomeAssistantError(
self.name, f"Setting state for entity {self.name} failed, state: {params}, error: {repr(err)}"
params, ) from err
repr(err), except InvalidAuthError:
) self.coordinator.entry.async_start_reauth(self.hass)

View File

@ -255,7 +255,7 @@ aiosenseme==0.6.1
aiosenz==1.0.0 aiosenz==1.0.0
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==3.0.0 aioshelly==4.0.0
# homeassistant.components.skybell # homeassistant.components.skybell
aioskybell==22.7.0 aioskybell==22.7.0

View File

@ -230,7 +230,7 @@ aiosenseme==0.6.1
aiosenz==1.0.0 aiosenz==1.0.0
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==3.0.0 aioshelly==4.0.0
# homeassistant.components.skybell # homeassistant.components.skybell
aioskybell==22.7.0 aioskybell==22.7.0

View File

@ -1,10 +1,11 @@
"""Test the Shelly config flow.""" """Test the Shelly config flow."""
import asyncio
from http import HTTPStatus
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import aiohttp from aioshelly.exceptions import (
import aioshelly DeviceConnectionError,
FirmwareUnsupported,
InvalidAuthError,
)
import pytest import pytest
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
@ -207,7 +208,7 @@ async def test_form_auth(hass, test_data):
@pytest.mark.parametrize( @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): async def test_form_errors_get_info(hass, error):
"""Test we handle errors.""" """Test we handle errors."""
@ -324,7 +325,7 @@ async def test_form_missing_model_key_zeroconf(hass, caplog):
@pytest.mark.parametrize( @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): async def test_form_errors_test_connection(hass, error):
"""Test we handle errors.""" """Test we handle errors."""
@ -431,10 +432,7 @@ async def test_form_firmware_unsupported(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with patch( with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported):
"aioshelly.common.get_info",
side_effect=aioshelly.exceptions.FirmwareUnsupported,
):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1"},
@ -447,15 +445,8 @@ async def test_form_firmware_unsupported(hass):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error", "error",
[ [
( (InvalidAuthError, "invalid_auth"),
aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST), (DeviceConnectionError, "cannot_connect"),
"cannot_connect",
),
(
aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED),
"invalid_auth",
),
(asyncio.TimeoutError, "cannot_connect"),
(ValueError, "unknown"), (ValueError, "unknown"),
], ],
) )
@ -490,15 +481,8 @@ async def test_form_auth_errors_test_connection_gen1(hass, error):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error", "error",
[ [
( (DeviceConnectionError, "cannot_connect"),
aioshelly.exceptions.JSONRPCError(code=400), (InvalidAuthError, "invalid_auth"),
"cannot_connect",
),
(
aioshelly.exceptions.InvalidAuthError(code=401),
"invalid_auth",
),
(asyncio.TimeoutError, "cannot_connect"),
(ValueError, "unknown"), (ValueError, "unknown"),
], ],
) )
@ -647,20 +631,8 @@ async def test_zeroconf_sleeping_device(hass):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize( async def test_zeroconf_sleeping_device_error(hass):
"error",
[
(
aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST),
"cannot_connect",
),
(asyncio.TimeoutError, "cannot_connect"),
],
)
async def test_zeroconf_sleeping_device_error(hass, error):
"""Test sleeping device configuration via zeroconf with error.""" """Test sleeping device configuration via zeroconf with error."""
exc = error
with patch( with patch(
"aioshelly.common.get_info", "aioshelly.common.get_info",
return_value={ return_value={
@ -671,7 +643,7 @@ async def test_zeroconf_sleeping_device_error(hass, error):
}, },
), patch( ), patch(
"aioshelly.block_device.BlockDevice.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock(side_effect=exc), new=AsyncMock(side_effect=DeviceConnectionError),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -708,10 +680,7 @@ async def test_zeroconf_already_configured(hass):
async def test_zeroconf_firmware_unsupported(hass): async def test_zeroconf_firmware_unsupported(hass):
"""Test we abort if device firmware is unsupported.""" """Test we abort if device firmware is unsupported."""
with patch( with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported):
"aioshelly.common.get_info",
side_effect=aioshelly.exceptions.FirmwareUnsupported,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
data=DISCOVERY_INFO, data=DISCOVERY_INFO,
@ -724,7 +693,7 @@ async def test_zeroconf_firmware_unsupported(hass):
async def test_zeroconf_cannot_connect(hass): async def test_zeroconf_cannot_connect(hass):
"""Test we get the form.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
data=DISCOVERY_INFO, data=DISCOVERY_INFO,
@ -840,21 +809,13 @@ async def test_reauth_successful(hass, test_data):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_data", "test_data",
[ [
( (1, {"username": "test user", "password": "test1 password"}),
1, (2, {"password": "test2 password"}),
{"username": "test user", "password": "test1 password"},
aioshelly.exceptions.InvalidAuthError(code=HTTPStatus.UNAUTHORIZED.value),
),
(
2,
{"password": "test2 password"},
aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED),
),
], ],
) )
async def test_reauth_unsuccessful(hass, test_data): async def test_reauth_unsuccessful(hass, test_data):
"""Test reauthentication flow failed.""" """Test reauthentication flow failed."""
gen, user_input, exc = test_data gen, user_input = test_data
entry = MockConfigEntry( entry = MockConfigEntry(
domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": gen} 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}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen},
), patch( ), patch(
"aioshelly.block_device.BlockDevice.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock(side_effect=exc), new=AsyncMock(side_effect=InvalidAuthError),
), patch( ), 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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -889,11 +851,7 @@ async def test_reauth_unsuccessful(hass, test_data):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error", "error",
[ [DeviceConnectionError, FirmwareUnsupported],
asyncio.TimeoutError,
aiohttp.ClientError,
aioshelly.exceptions.FirmwareUnsupported,
],
) )
async def test_reauth_get_info_error(hass, error): async def test_reauth_get_info_error(hass, error):
"""Test reauthentication flow failed with error in get_info().""" """Test reauthentication flow failed with error in get_info()."""