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
pull/80671/head
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."""
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

View File

@ -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()

View File

@ -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
)

View File

@ -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"

View File

@ -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:

View File

@ -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):

View File

@ -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": [
{

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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()."""