From 291df6dafe44090cb13ebae4672d2c1de4ba6e9c Mon Sep 17 00:00:00 2001 From: Shay Levy <levyshay1@gmail.com> Date: Sun, 14 Apr 2024 18:07:26 +0300 Subject: [PATCH] Bump aioshelly to 9.0.0 (#114025) * Update Shelly to use initialize from aioshelly * Save indentation in _async_device_connect * Use firmware_supported property from aioshelly * Review comments * Bump aioshelly * Fix lint errors * Test RPC initialized update --- homeassistant/components/shelly/__init__.py | 116 +++++------------- .../components/shelly/config_flow.py | 11 +- .../components/shelly/coordinator.py | 89 ++++++++++---- homeassistant/components/shelly/entity.py | 2 +- homeassistant/components/shelly/light.py | 2 +- homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/strings.json | 1 - homeassistant/components/shelly/switch.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/conftest.py | 18 +++ tests/components/shelly/test_binary_sensor.py | 13 +- tests/components/shelly/test_climate.py | 18 +-- tests/components/shelly/test_config_flow.py | 50 +------- tests/components/shelly/test_coordinator.py | 115 ++++++++++------- tests/components/shelly/test_init.py | 69 ++++++++--- tests/components/shelly/test_number.py | 44 +++++-- tests/components/shelly/test_sensor.py | 33 +++-- tests/components/shelly/test_update.py | 11 +- 19 files changed, 349 insertions(+), 257 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 7d23a1cd57d..cfeab531687 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -3,23 +3,22 @@ from __future__ import annotations import contextlib -from typing import Any, Final +from typing import Final -from aioshelly.block_device import BlockDevice, BlockUpdateType +from aioshelly.block_device import BlockDevice from aioshelly.common import ConnectionOptions from aioshelly.const import DEFAULT_COAP_PORT, RPC_GENERATIONS from aioshelly.exceptions import ( DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, MacAddressMismatchError, ) -from aioshelly.rpc_device import RpcDevice, RpcUpdateType +from aioshelly.rpc_device import RpcDevice import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -53,12 +52,9 @@ from .coordinator import ( ) from .utils import ( async_create_issue_unsupported_firmware, - async_shutdown_device, - get_block_device_sleep_period, get_coap_context, get_device_entry_gen, get_http_port, - get_rpc_device_wakeup_period, get_ws_context, ) @@ -154,7 +150,6 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b async_get_clientsession(hass), coap_context, options, - False, ) dev_reg = dr_async_get(hass) @@ -186,57 +181,38 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b data[CONF_SLEEP_PERIOD] = sleep_period = BLOCK_EXPECTED_SLEEP_PERIOD hass.config_entries.async_update_entry(entry, data=data) - async def _async_block_device_setup() -> None: - """Set up a block based device that is online.""" - shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) - shelly_entry_data.block.async_setup() - - platforms = BLOCK_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) - platforms = BLOCK_PLATFORMS - - await hass.config_entries.async_forward_entry_setups(entry, platforms) - - @callback - def _async_device_online(_: Any, update_type: BlockUpdateType) -> None: - LOGGER.debug("Device %s is online, resuming setup", entry.title) - shelly_entry_data.device = None - - if sleep_period is None: - data = {**entry.data} - data[CONF_SLEEP_PERIOD] = get_block_device_sleep_period(device.settings) - data["model"] = device.settings["device"]["type"] - hass.config_entries.async_update_entry(entry, data=data) - - hass.async_create_task(_async_block_device_setup(), eager_start=True) - if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online block device %s", entry.title) try: await device.initialize() + if not device.firmware_supported: + async_create_issue_unsupported_firmware(hass, entry) + raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - except FirmwareUnsupported as err: - async_create_issue_unsupported_firmware(hass, entry) - raise ConfigEntryNotReady from err - await _async_block_device_setup() + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup() + shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) + await hass.config_entries.async_forward_entry_setups(entry, BLOCK_PLATFORMS) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device - shelly_entry_data.device = device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - device.subscribe_updates(_async_device_online) + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup(BLOCK_SLEEPING_PLATFORMS) else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - await _async_block_device_setup() + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup() + await hass.config_entries.async_forward_entry_setups( + entry, BLOCK_SLEEPING_PLATFORMS + ) ir.async_delete_issue( hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id) @@ -260,7 +236,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo async_get_clientsession(hass), ws_context, options, - False, ) dev_reg = dr_async_get(hass) @@ -276,58 +251,38 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] - async def _async_rpc_device_setup() -> None: - """Set up a RPC based device that is online.""" - shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) - shelly_entry_data.rpc.async_setup() - - platforms = RPC_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator( - hass, entry, device - ) - platforms = RPC_PLATFORMS - - await hass.config_entries.async_forward_entry_setups(entry, platforms) - - @callback - def _async_device_online(_: Any, update_type: RpcUpdateType) -> None: - LOGGER.debug("Device %s is online, resuming setup", entry.title) - shelly_entry_data.device = None - - if sleep_period is None: - data = {**entry.data} - data[CONF_SLEEP_PERIOD] = get_rpc_device_wakeup_period(device.status) - hass.config_entries.async_update_entry(entry, data=data) - - hass.async_create_task(_async_rpc_device_setup(), eager_start=True) - if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online RPC device %s", entry.title) try: await device.initialize() - except FirmwareUnsupported as err: - async_create_issue_unsupported_firmware(hass, entry) - raise ConfigEntryNotReady from err + if not device.firmware_supported: + async_create_issue_unsupported_firmware(hass, entry) + raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - await _async_rpc_device_setup() + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup() + shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device) + await hass.config_entries.async_forward_entry_setups(entry, RPC_PLATFORMS) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device - shelly_entry_data.device = device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - device.subscribe_updates(_async_device_online) + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup(RPC_SLEEPING_PLATFORMS) else: # Restore sensors for sleeping device - LOGGER.debug("Setting up offline block device %s", entry.title) - await _async_rpc_device_setup() + LOGGER.debug("Setting up offline RPC device %s", entry.title) + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup() + await hass.config_entries.async_forward_entry_setups( + entry, RPC_SLEEPING_PLATFORMS + ) ir.async_delete_issue( hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id) @@ -339,11 +294,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" shelly_entry_data = get_entry_data(hass)[entry.entry_id] - # If device is present, block/rpc coordinator is not setup yet - if (device := shelly_entry_data.device) is not None: - await async_shutdown_device(device) - return True - platforms = RPC_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): platforms = RPC_PLATFORMS diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 24b66e15893..46cea4e49a4 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -11,7 +11,6 @@ from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATION from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, ) from aioshelly.rpc_device import RpcDevice @@ -103,6 +102,7 @@ async def validate_input( ws_context, options, ) + await rpc_device.initialize() await rpc_device.shutdown() sleep_period = get_rpc_device_wakeup_period(rpc_device.status) @@ -121,6 +121,7 @@ async def validate_input( coap_context, options, ) + await block_device.initialize() block_device.shutdown() return { "title": block_device.name, @@ -154,8 +155,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self.info = await self._async_get_info(host, port) except DeviceConnectionError: errors["base"] = "cannot_connect" - except FirmwareUnsupported: - return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -287,8 +286,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self.info = await self._async_get_info(host, DEFAULT_HTTP_PORT) except DeviceConnectionError: return self.async_abort(reason="cannot_connect") - except FirmwareUnsupported: - return self.async_abort(reason="unsupported_firmware") if not mac: # We could not get the mac address from the name @@ -366,14 +363,14 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await self._async_get_info(host, port) - except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): + except (DeviceConnectionError, InvalidAuthError): return self.async_abort(reason="reauth_unsuccessful") if get_device_entry_gen(self.entry) != 1: user_input[CONF_USERNAME] = "admin" try: await validate_input(self.hass, host, port, info, user_input) - except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): + except (DeviceConnectionError, InvalidAuthError): return self.async_abort(reason="reauth_unsuccessful") return self.async_update_reload_and_abort( diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 18f96dd9c2e..bd6686198ed 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -15,7 +15,12 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from aioshelly.rpc_device import RpcDevice, RpcUpdateType from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_HOST, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.debounce import Debouncer @@ -58,7 +63,9 @@ from .const import ( BLEScannerMode, ) from .utils import ( + async_create_issue_unsupported_firmware, async_shutdown_device, + get_block_device_sleep_period, get_device_entry_gen, get_http_port, get_rpc_device_wakeup_period, @@ -73,7 +80,6 @@ class ShellyEntryData: """Class for sharing data within a given config entry.""" block: ShellyBlockCoordinator | None = None - device: BlockDevice | RpcDevice | None = None rest: ShellyRestCoordinator | None = None rpc: ShellyRpcCoordinator | None = None rpc_poll: ShellyRpcPollingCoordinator | None = None @@ -98,6 +104,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): self.entry = entry self.device = device self.device_id: str | None = None + self._pending_platforms: list[Platform] | None = None device_name = device.name if device.initialized else entry.title interval_td = timedelta(seconds=update_interval) super().__init__(hass, LOGGER, name=device_name, update_interval=interval_td) @@ -131,8 +138,9 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): """Sleep period of the device.""" return self.entry.data.get(CONF_SLEEP_PERIOD, 0) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" + self._pending_platforms = pending_platforms dev_reg = dr_async_get(self.hass) device_entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, @@ -146,6 +154,45 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): ) self.device_id = device_entry.id + async def _async_device_connect(self) -> None: + """Connect to a Shelly Block device.""" + LOGGER.debug("Connecting to Shelly Device - %s", self.name) + try: + await self.device.initialize() + update_device_fw_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) + return + + if not self.device.firmware_supported: + async_create_issue_unsupported_firmware(self.hass, self.entry) + return + + if not self._pending_platforms: + return + + LOGGER.debug("Device %s is online, resuming setup", self.entry.title) + platforms = self._pending_platforms + self._pending_platforms = None + + data = {**self.entry.data} + + # Update sleep_period + old_sleep_period = data[CONF_SLEEP_PERIOD] + if isinstance(self.device, RpcDevice): + new_sleep_period = get_rpc_device_wakeup_period(self.device.status) + elif isinstance(self.device, BlockDevice): + new_sleep_period = get_block_device_sleep_period(self.device.settings) + + if new_sleep_period != old_sleep_period: + data[CONF_SLEEP_PERIOD] = new_sleep_period + self.hass.config_entries.async_update_entry(self.entry, data=data) + + # Resume platform setup + await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) + async def _async_reload_entry(self) -> None: """Reload entry.""" self._debounced_reload.async_cancel() @@ -179,7 +226,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self._last_cfg_changed: int | None = None self._last_mode: str | None = None - self._last_effect: int | None = None + self._last_effect: str | None = None self._last_input_events_count: dict = {} self._last_target_temp: float | None = None self._push_update_failures: int = 0 @@ -211,15 +258,14 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): if not self.device.initialized: return - assert self.device.blocks - # For buttons which are battery powered - set initial value for last_event_count if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None: for block in self.device.blocks: if block.type != "device": continue - if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button": + wakeup_event = cast(list, block.wakeupEvent) + if len(wakeup_event) == 1 and wakeup_event[0] == "button": self._last_input_events_count[1] = -1 break @@ -228,7 +274,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): cfg_changed = 0 for block in self.device.blocks: if block.type == "device" and block.cfgChanged is not None: - cfg_changed = block.cfgChanged + cfg_changed = cast(int, block.cfgChanged) # Shelly TRV sends information about changing the configuration for no # reason, reloading the config entry is not needed for it. @@ -314,14 +360,16 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self, device_: BlockDevice, update_type: BlockUpdateType ) -> None: """Handle device update.""" - if update_type == BlockUpdateType.COAP_PERIODIC: + if update_type is BlockUpdateType.ONLINE: + self.hass.async_create_task(self._async_device_connect(), eager_start=True) + elif update_type is BlockUpdateType.COAP_PERIODIC: self._push_update_failures = 0 ir.async_delete_issue( self.hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), ) - elif update_type == BlockUpdateType.COAP_REPLY: + elif update_type is BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: LOGGER.debug( @@ -346,9 +394,9 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): ) self.async_set_updated_data(None) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" - super().async_setup() + super().async_setup(pending_platforms) self.device.subscribe_updates(self._async_handle_update) def shutdown(self) -> None: @@ -538,14 +586,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: return - LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) - try: - await self.device.initialize() - update_device_fw_info(self.hass, self.device, self.entry) - except DeviceConnectionError as err: - raise UpdateFailed(f"Device disconnected: {repr(err)}") from err - except InvalidAuthError: - await self.async_shutdown_device_and_start_reauth() + await self._async_device_connect() async def _async_disconnected(self) -> None: """Handle device disconnected.""" @@ -612,7 +653,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self, device_: RpcDevice, update_type: RpcUpdateType ) -> None: """Handle device update.""" - if update_type is RpcUpdateType.INITIALIZED: + if update_type is RpcUpdateType.ONLINE: + self.hass.async_create_task(self._async_device_connect(), eager_start=True) + elif update_type is RpcUpdateType.INITIALIZED: self.hass.async_create_task(self._async_connected(), eager_start=True) self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: @@ -624,9 +667,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): elif update_type is RpcUpdateType.EVENT and (event := self.device.event): self._async_device_event_handler(event) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" - super().async_setup() + super().async_setup(pending_platforms) self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index accca5f1a64..150244e2e47 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -74,7 +74,7 @@ def async_setup_block_attribute_entities( for block in coordinator.device.blocks: for sensor_id in block.sensor_ids: - description = sensors.get((block.type, sensor_id)) + description = sensors.get((cast(str, block.type), sensor_id)) if description is None: continue diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index d0590fc7c20..0650e2d15e5 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -78,7 +78,7 @@ def async_setup_block_entry( for block in coordinator.device.blocks: if block.type == "light": blocks.append(block) - elif block.type == "relay": + elif block.type == "relay" and block.channel is not None: if not is_block_channel_type_light( coordinator.device.settings, int(block.channel) ): diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 06159cb543b..08971713ced 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==8.2.0"], + "requirements": ["aioshelly==9.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index d4a8b117f4c..cee27e9ca07 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -38,7 +38,6 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "unsupported_firmware": "The device is using an unsupported firmware version.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." } diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 48ff337d22a..14fec43c58b 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -104,8 +104,12 @@ def async_setup_block_entry( relay_blocks = [] assert coordinator.device.blocks for block in coordinator.device.blocks: - if block.type != "relay" or is_block_channel_type_light( - coordinator.device.settings, int(block.channel) + if ( + block.type != "relay" + or block.channel is not None + and is_block_channel_type_light( + coordinator.device.settings, int(block.channel) + ) ): continue diff --git a/requirements_all.txt b/requirements_all.txt index 54d136c0b83..14f5749a02a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -368,7 +368,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.2.0 +aioshelly==9.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32bfdcd0968..a58259c096c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.2.0 +aioshelly==9.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 3cd27101f76..18813ff7eba 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -319,6 +319,11 @@ async def mock_block_device(): {}, BlockUpdateType.COAP_REPLY ) + def online(): + block_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, BlockUpdateType.ONLINE + ) + device = Mock( spec=BlockDevice, blocks=MOCK_BLOCKS, @@ -337,6 +342,7 @@ async def mock_block_device(): block_device_mock.return_value.mock_update_reply = Mock( side_effect=update_reply ) + block_device_mock.return_value.mock_online = Mock(side_effect=online) yield block_device_mock.return_value @@ -376,16 +382,28 @@ async def mock_rpc_device(): {}, RpcUpdateType.EVENT ) + def online(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, RpcUpdateType.ONLINE + ) + def disconnected(): rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( {}, RpcUpdateType.DISCONNECTED ) + def initialized(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, RpcUpdateType.INITIALIZED + ) + device = _mock_rpc_device() rpc_device_mock.return_value = device rpc_device_mock.return_value.mock_disconnected = Mock(side_effect=disconnected) rpc_device_mock.return_value.mock_update = Mock(side_effect=update) rpc_device_mock.return_value.mock_event = Mock(side_effect=event) + rpc_device_mock.return_value.mock_online = Mock(side_effect=online) + rpc_device_mock.return_value.mock_initialized = Mock(side_effect=initialized) yield rpc_device_mock.return_value diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 00a430cd4b1..624eb82f060 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -144,7 +144,7 @@ async def test_block_sleeping_binary_sensor( assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -180,7 +180,7 @@ async def test_block_restored_sleeping_binary_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -206,7 +206,7 @@ async def test_block_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -263,6 +263,7 @@ async def test_rpc_sleeping_binary_sensor( ) -> None: """Test RPC online sleeping binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_cloud" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) config_entry = await init_integration(hass, 2, sleep_period=1000) # Sensor should be created when device is online @@ -273,7 +274,7 @@ async def test_rpc_sleeping_binary_sensor( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -344,6 +345,10 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 0bdab979a0e..9fee3468f11 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -64,7 +64,7 @@ async def test_climate_hvac_mode( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() # Test initial hvac mode - off @@ -125,7 +125,7 @@ async def test_climate_set_temperature( await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -192,7 +192,7 @@ async def test_climate_set_preset_mode( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -278,7 +278,7 @@ async def test_block_restored_climate( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == HVACMode.OFF @@ -349,7 +349,7 @@ async def test_block_restored_climate_us_customery( monkeypatch.setattr(mock_block_device, "initialized", True) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == HVACMode.OFF @@ -451,7 +451,7 @@ async def test_block_set_mode_connection_error( await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() with pytest.raises(HomeAssistantError): @@ -476,7 +476,7 @@ async def test_block_set_mode_auth_error( entry = await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED @@ -534,7 +534,7 @@ async def test_block_restored_climate_auth_error( type(mock_block_device).settings = PropertyMock( return_value={}, side_effect=InvalidAuthError ) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED @@ -561,7 +561,7 @@ async def test_device_not_calibrated( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() mock_status = MOCK_STATUS_COAP.copy() diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index f2b0736f867..c73b93f9fdb 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -10,7 +10,6 @@ from aioshelly.const import DEFAULT_HTTP_PORT, MODEL_1, MODEL_PLUS_2PM from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, ) import pytest @@ -433,25 +432,6 @@ async def test_user_setup_ignored_device( assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_firmware_unsupported(hass: HomeAssistant) -> None: - """Test we abort if device firmware is unsupported.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.shelly.config_flow.get_info", - side_effect=FirmwareUnsupported, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "1.1.1.1"}, - ) - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "unsupported_firmware" - - @pytest.mark.parametrize( ("exc", "base_error"), [ @@ -757,22 +737,6 @@ async def test_zeroconf_with_wifi_ap_ip(hass: HomeAssistant) -> None: assert entry.data["host"] == "2.2.2.2" -async def test_zeroconf_firmware_unsupported(hass: HomeAssistant) -> None: - """Test we abort if device firmware is unsupported.""" - with patch( - "homeassistant.components.shelly.config_flow.get_info", - side_effect=FirmwareUnsupported, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=DISCOVERY_INFO, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unsupported_firmware" - - async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: """Test we get the form.""" with patch( @@ -927,11 +891,7 @@ async def test_reauth_unsuccessful( assert result["reason"] == "reauth_unsuccessful" -@pytest.mark.parametrize( - "error", - [DeviceConnectionError, FirmwareUnsupported], -) -async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> None: +async def test_reauth_get_info_error(hass: HomeAssistant) -> None: """Test reauthentication flow failed with error in get_info().""" entry = MockConfigEntry( domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": 2} @@ -940,7 +900,7 @@ async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> N with patch( "homeassistant.components.shelly.config_flow.get_info", - side_effect=error, + side_effect=DeviceConnectionError, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1154,6 +1114,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( caplog: pytest.LogCaptureFixture, ) -> None: """Test zeroconf discovery does not triggers refresh for sleeping device.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = MockConfigEntry( domain="shelly", unique_id="AABBCCDDEEFF", @@ -1163,10 +1124,11 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert "online, resuming setup" in caplog.text + assert len(mock_rpc_device.initialize.mock_calls) == 1 with patch( "homeassistant.components.shelly.config_flow.get_info", @@ -1186,7 +1148,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) ) await hass.async_block_till_done() - assert len(mock_rpc_device.initialize.mock_calls) == 0 + assert len(mock_rpc_device.initialize.mock_calls) == 1 assert "device did not update" not in caplog.text diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index b155176dccd..9f251d1e008 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -1,14 +1,10 @@ """Tests for Shelly coordinator.""" from datetime import timedelta -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, call, patch from aioshelly.const import MODEL_BULB, MODEL_BUTTON1 -from aioshelly.exceptions import ( - DeviceConnectionError, - FirmwareUnsupported, - InvalidAuthError, -) +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from freezegun.api import FrozenDateTimeFactory import pytest @@ -29,13 +25,13 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, async_entries_for_config_entry, async_get as async_get_dev_reg, format_mac, ) -import homeassistant.helpers.issue_registry as ir from . import ( MOCK_MAC, @@ -216,28 +212,25 @@ async def test_block_rest_update_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -async def test_block_firmware_unsupported( +async def test_block_sleeping_device_firmware_unsupported( hass: HomeAssistant, - freezer: FrozenDateTimeFactory, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, ) -> None: - """Test block device polling authentication error.""" - monkeypatch.setattr( - mock_block_device, - "update", - AsyncMock(side_effect=FirmwareUnsupported), - ) - entry = await init_integration(hass, 1) - - assert entry.state is ConfigEntryState.LOADED + """Test block sleeping device firmware not supported.""" + monkeypatch.setattr(mock_block_device, "firmware_supported", False) + entry = await init_integration(hass, 1, sleep_period=3600) - # Move time to generate polling - freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15)) - async_fire_time_changed(hass) + # Make device online + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues async def test_block_polling_connection_error( @@ -290,20 +283,28 @@ async def test_block_rest_update_connection_error( async def test_block_sleeping_device_no_periodic_updates( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert get_entity_state(hass, entity_id) == "22.1" # Move time to generate polling - freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 3600)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -352,7 +353,7 @@ async def test_block_button_click_event( entry = await init_integration(hass, 1, model=MODEL_BUTTON1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() dev_reg = async_get_dev_reg(hass) @@ -529,6 +530,7 @@ async def test_rpc_update_entry_sleep_period( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC update entry sleep period.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 600) entry = await init_integration(hass, 2, sleep_period=600) register_entity( hass, @@ -539,7 +541,7 @@ async def test_rpc_update_entry_sleep_period( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert entry.data["sleep_period"] == 600 @@ -554,10 +556,14 @@ async def test_rpc_update_entry_sleep_period( async def test_rpc_sleeping_device_no_periodic_updates( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) register_entity( hass, @@ -568,7 +574,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert get_entity_state(hass, entity_id) == "22.9" @@ -581,25 +587,25 @@ async def test_rpc_sleeping_device_no_periodic_updates( assert get_entity_state(hass, entity_id) is STATE_UNAVAILABLE -async def test_rpc_firmware_unsupported( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock +async def test_rpc_sleeping_device_firmware_unsupported( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, ) -> None: - """Test RPC update entry unsupported firmware.""" - entry = await init_integration(hass, 2) - register_entity( - hass, - SENSOR_DOMAIN, - "test_name_temperature", - "temperature:0-temperature_0", - entry, - ) + """Test RPC sleeping device firmware not supported.""" + monkeypatch.setattr(mock_rpc_device, "firmware_supported", False) + entry = await init_integration(hass, 2, sleep_period=3600) - # Move time to generate sleep period update - freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) - async_fire_time_changed(hass) + # Make device online + mock_rpc_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues async def test_rpc_reconnect_auth_error( @@ -753,11 +759,12 @@ async def test_rpc_update_entry_fw_ver( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test RPC update entry firmware version.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 600) entry = await init_integration(hass, 2, sleep_period=600) dev_reg = async_get_dev_reg(hass) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert entry.unique_id @@ -779,3 +786,23 @@ async def test_rpc_update_entry_fw_ver( ) assert device assert device.sw_version == "99.0.0" + + +async def test_rpc_runs_connected_events_when_initialized( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC runs connected events when initialized.""" + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await init_integration(hass, 2) + + assert call.script_list() not in mock_rpc_device.mock_calls + + # Mock initialized event + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_initialized() + await hass.async_block_till_done() + + # BLE script list is called during connected events + assert call.script_list() in mock_rpc_device.mock_calls diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index de658cd0d16..61ec8ce6779 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -8,7 +8,6 @@ from aioshelly.common import ConnectionOptions from aioshelly.const import MODEL_PLUS_2PM from aioshelly.exceptions import ( DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, MacAddressMismatchError, ) @@ -27,6 +26,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceRegistry, @@ -145,27 +145,46 @@ async def test_setup_entry_not_shelly( @pytest.mark.parametrize("gen", [1, 2, 3]) -@pytest.mark.parametrize("side_effect", [DeviceConnectionError, FirmwareUnsupported]) async def test_device_connection_error( hass: HomeAssistant, gen: int, - side_effect: Exception, mock_block_device: Mock, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test device connection error.""" monkeypatch.setattr( - mock_block_device, "initialize", AsyncMock(side_effect=side_effect) + mock_block_device, "initialize", AsyncMock(side_effect=DeviceConnectionError) ) monkeypatch.setattr( - mock_rpc_device, "initialize", AsyncMock(side_effect=side_effect) + mock_rpc_device, "initialize", AsyncMock(side_effect=DeviceConnectionError) ) entry = await init_integration(hass, gen) assert entry.state is ConfigEntryState.SETUP_RETRY +@pytest.mark.parametrize("gen", [1, 2, 3]) +async def test_device_unsupported_firmware( + hass: HomeAssistant, + gen: int, + mock_block_device: Mock, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, +) -> None: + """Test device init with unsupported firmware.""" + monkeypatch.setattr(mock_block_device, "firmware_supported", False) + monkeypatch.setattr(mock_rpc_device, "firmware_supported", False) + + entry = await init_integration(hass, gen) + assert entry.state is ConfigEntryState.SETUP_RETRY + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues + + @pytest.mark.parametrize("gen", [1, 2, 3]) async def test_mac_mismatch_error( hass: HomeAssistant, @@ -217,12 +236,13 @@ async def test_device_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -@pytest.mark.parametrize(("entry_sleep", "device_sleep"), [(None, 0), (1000, 1000)]) +@pytest.mark.parametrize(("entry_sleep", "device_sleep"), [(None, 0), (3600, 3600)]) async def test_sleeping_block_device_online( hass: HomeAssistant, entry_sleep: int | None, device_sleep: int, mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, device_reg: DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: @@ -234,10 +254,17 @@ async def test_sleeping_block_device_online( connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": int(device_sleep / 60), "unit": "m"}, + ) entry = await init_integration(hass, 1, sleep_period=entry_sleep) assert "will resume when device is online" in caplog.text - mock_block_device.mock_update() + mock_block_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -248,13 +275,17 @@ async def test_sleeping_rpc_device_online( entry_sleep: int | None, device_sleep: int, mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test sleeping RPC device online.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", device_sleep) entry = await init_integration(hass, 2, sleep_period=entry_sleep) assert "will resume when device is online" in caplog.text - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -270,7 +301,9 @@ async def test_sleeping_rpc_device_online_new_firmware( assert "will resume when device is online" in caplog.text mutate_rpc_device_status(monkeypatch, mock_rpc_device, "sys", "wakeup_period", 1500) - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == 1500 @@ -413,9 +446,12 @@ async def test_entry_missing_port(hass: HomeAssistant) -> None: } entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC) entry.add_to_hass(hass) - with patch( - "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() - ) as rpc_device_mock: + with ( + patch("homeassistant.components.shelly.RpcDevice.initialize"), + patch( + "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() + ) as rpc_device_mock, + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -435,9 +471,12 @@ async def test_rpc_entry_custom_port(hass: HomeAssistant) -> None: } entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC) entry.add_to_hass(hass) - with patch( - "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() - ) as rpc_device_mock: + with ( + patch("homeassistant.components.shelly.RpcDevice.initialize"), + patch( + "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() + ) as rpc_device_mock, + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index c138ef71b7d..99ad5709d29 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -33,12 +33,17 @@ async def test_block_number_update( ) -> None: """Test block device number update.""" entity_id = "number.test_name_valve_position" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "50" @@ -93,7 +98,7 @@ async def test_block_restored_number( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "50" @@ -130,20 +135,27 @@ async def test_block_restored_number_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "50" async def test_block_number_set_value( - hass: HomeAssistant, mock_block_device: Mock + hass: HomeAssistant, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device number set value.""" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() mock_block_device.reset_mock() @@ -162,15 +174,20 @@ async def test_block_set_value_connection_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device set value connection error.""" + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) monkeypatch.setattr( mock_block_device, "http_request", AsyncMock(side_effect=DeviceConnectionError), ) - await init_integration(hass, 1, sleep_period=1000) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() with pytest.raises(HomeAssistantError): @@ -186,15 +203,20 @@ async def test_block_set_value_auth_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device set value authentication error.""" + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) monkeypatch.setattr( mock_block_device, "http_request", AsyncMock(side_effect=InvalidAuthError), ) - entry = await init_integration(hass, 1, sleep_period=1000) + entry = await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 0a15b78994b..6151cac10ab 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -164,7 +164,7 @@ async def test_block_sleeping_sensor( assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" @@ -206,7 +206,7 @@ async def test_block_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" @@ -232,7 +232,7 @@ async def test_block_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" @@ -305,7 +305,7 @@ async def test_block_not_matched_restored_sleeping_sensor( mock_block_device.blocks[SENSOR_BLOCK_ID], "description", "other_desc" ) monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "20.4" @@ -448,6 +448,7 @@ async def test_rpc_sleeping_sensor( ) -> None: """Test RPC online sleeping sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) # Sensor should be created when device is online @@ -462,7 +463,7 @@ async def test_rpc_sleeping_sensor( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.9" @@ -501,6 +502,10 @@ async def test_rpc_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -533,6 +538,10 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -583,19 +592,21 @@ async def test_rpc_sleeping_update_entity_service( hass: HomeAssistant, mock_rpc_device: Mock, entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test RPC sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) await init_integration(hass, 2, sleep_period=1000) # Entity should be created when device is online assert hass.states.get(entity_id) is None # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -627,19 +638,25 @@ async def test_block_sleeping_update_entity_service( hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test block sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Sensor should be created when device is online assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 73320b2c65f..93b0f55c415 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -335,6 +335,7 @@ async def test_rpc_sleeping_update( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC sleeping device update entity.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( mock_rpc_device.status["sys"], @@ -350,7 +351,7 @@ async def test_rpc_sleeping_update( assert hass.states.get(entity_id) is None # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -411,6 +412,10 @@ async def test_rpc_restored_sleeping_update( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -456,6 +461,10 @@ async def test_rpc_restored_sleeping_update_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() -- GitLab