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 entitypull/80671/head
parent
2c43606922
commit
aea7a9af18
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()."""
|
||||
|
|
Loading…
Reference in New Issue