From be206156b0c6c8dea1bc61d3817e94bf9e52dfe6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Aug 2024 09:21:23 -1000 Subject: [PATCH] Fix shelly sleepy entities never being created (#124547) --- homeassistant/components/shelly/__init__.py | 19 +++++++- tests/components/shelly/test_binary_sensor.py | 36 +++++++++++---- tests/components/shelly/test_climate.py | 15 ++++--- tests/components/shelly/test_coordinator.py | 44 +++++++++++++++++-- tests/components/shelly/test_number.py | 6 ++- tests/components/shelly/test_sensor.py | 33 ++++++++++---- tests/components/shelly/test_switch.py | 6 ++- tests/components/shelly/test_update.py | 6 ++- 8 files changed, 132 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 994b1ed0430..1d3f67220fa 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, device_registry as dr, + entity_registry as er, issue_registry as ir, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -192,8 +193,15 @@ async def _async_setup_block_entry( await hass.config_entries.async_forward_entry_setups( entry, runtime_data.platforms ) - elif sleep_period is None or device_entry is None: + elif ( + sleep_period is None + or device_entry is None + or not er.async_entries_for_device(er.async_get(hass), device_entry.id) + ): # Need to get sleep info or first time sleeping device setup, wait for device + # If there are no entities for the device, it means we added the device, but + # Home Assistant was restarted before the device was online. In this case we + # cannot restore the entities, so we need to wait for the device to be online. LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) @@ -268,8 +276,15 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) await hass.config_entries.async_forward_entry_setups( entry, runtime_data.platforms ) - elif sleep_period is None or device_entry is None: + elif ( + sleep_period is None + or device_entry is None + or not er.async_entries_for_device(er.async_get(hass), device_entry.id) + ): # Need to get sleep info or first time sleeping device setup, wait for device + # If there are no entities for the device, it means we added the device, but + # Home Assistant was restarted before the device was online. In this case we + # cannot restore the entities, so we need to wait for the device to be online. LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 18f65deb907..fadfe28db3e 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -169,9 +169,14 @@ async def test_block_restored_sleeping_binary_sensor( ) -> None: """Test block restored sleeping binary sensor.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + hass, + BINARY_SENSOR_DOMAIN, + "test_name_motion", + "sensor_0-motion", + entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, STATE_ON)]) monkeypatch.setattr(mock_block_device, "initialized", False) @@ -196,9 +201,14 @@ async def test_block_restored_sleeping_binary_sensor_no_last_state( ) -> None: """Test block restored sleeping binary sensor missing last state.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + hass, + BINARY_SENSOR_DOMAIN, + "test_name_motion", + "sensor_0-motion", + entry, + device_id=device.id, ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) @@ -305,9 +315,14 @@ async def test_rpc_restored_sleeping_binary_sensor( ) -> None: """Test RPC restored binary sensor.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + hass, + BINARY_SENSOR_DOMAIN, + "test_name_cloud", + "cloud-cloud", + entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, STATE_ON)]) @@ -334,9 +349,14 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state( ) -> None: """Test RPC restored sleeping binary sensor missing last state.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + hass, + BINARY_SENSOR_DOMAIN, + "test_name_cloud", + "cloud-cloud", + entry, + device_id=device.id, ) monkeypatch.setattr(mock_rpc_device, "initialized", False) diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index fea46b1d2d1..1156d7e0ed5 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -254,13 +254,14 @@ async def test_block_restored_climate( monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp") entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, "test_name", "sensor_0", entry, + device_id=device.id, ) attrs = {"current_temperature": 20.5, "temperature": 4.0} extra_data = {"last_target_temp": 22.0} @@ -321,13 +322,14 @@ async def test_block_restored_climate_us_customery( monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp") entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, "test_name", "sensor_0", entry, + device_id=device.id, ) attrs = {"current_temperature": 67, "temperature": 39} extra_data = {"last_target_temp": 10.0} @@ -390,13 +392,14 @@ async def test_block_restored_climate_unavailable( monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, "test_name", "sensor_0", entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, STATE_UNAVAILABLE)]) @@ -417,13 +420,14 @@ async def test_block_restored_climate_set_preset_before_online( monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, "test_name", "sensor_0", entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) @@ -518,13 +522,14 @@ async def test_block_restored_climate_auth_error( monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, CLIMATE_DOMAIN, "test_name", "sensor_0", entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1140c93775b..bb9694cf9b4 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -16,6 +16,7 @@ from homeassistant.components.shelly.const import ( ATTR_DEVICE, ATTR_GENERATION, CONF_BLE_SCANNER_MODE, + CONF_SLEEP_PERIOD, DOMAIN, ENTRY_RELOAD_COOLDOWN, MAX_PUSH_UPDATE_FAILURES, @@ -886,9 +887,14 @@ async def test_block_sleeping_device_connection_error( """Test block sleeping device connection error during initialize.""" sleep_period = 1000 entry = await init_integration(hass, 1, sleep_period=sleep_period, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + hass, + BINARY_SENSOR_DOMAIN, + "test_name_motion", + "sensor_0-motion", + entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, STATE_ON)]) monkeypatch.setattr(mock_block_device, "initialized", False) @@ -931,9 +937,14 @@ async def test_rpc_sleeping_device_connection_error( """Test RPC sleeping device connection error during initialize.""" sleep_period = 1000 entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + hass, + BINARY_SENSOR_DOMAIN, + "test_name_cloud", + "cloud-cloud", + entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, STATE_ON)]) monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -966,6 +977,31 @@ async def test_rpc_sleeping_device_connection_error( assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE +async def test_rpc_sleeping_device_late_setup( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC sleeping device creates entities if they do not exist yet.""" + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) + assert entry.data[CONF_SLEEP_PERIOD] == 1000 + register_device(device_registry, entry) + monkeypatch.setattr(mock_rpc_device, "connected", False) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + monkeypatch.setattr(mock_rpc_device, "connected", True) + mock_rpc_device.mock_initialized() + await hass.async_block_till_done(wait_background_tasks=True) + assert hass.states.get("sensor.test_name_temperature") is not None + + async def test_rpc_already_connected( hass: HomeAssistant, freezer: FrozenDateTimeFactory, diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 73f432094b9..6c1cc394b64 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -72,7 +72,7 @@ async def test_block_restored_number( ) -> None: """Test block restored number.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) capabilities = { "min": 0, "max": 100, @@ -86,6 +86,7 @@ async def test_block_restored_number( "device_0-valvePos", entry, capabilities, + device_id=device.id, ) extra_data = { "native_max_value": 100, @@ -118,7 +119,7 @@ async def test_block_restored_number_no_last_state( ) -> None: """Test block restored number missing last state.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) capabilities = { "min": 0, "max": 100, @@ -132,6 +133,7 @@ async def test_block_restored_number_no_last_state( "device_0-valvePos", entry, capabilities, + device_id=device.id, ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 7aa4218341f..6b665b8fea2 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -193,9 +193,14 @@ async def test_block_restored_sleeping_sensor( ) -> None: """Test block restored sleeping sensor.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry + hass, + SENSOR_DOMAIN, + "test_name_temperature", + "sensor_0-temp", + entry, + device_id=device.id, ) extra_data = {"native_value": "20.4", "native_unit_of_measurement": "°C"} @@ -226,9 +231,14 @@ async def test_block_restored_sleeping_sensor_no_last_state( ) -> None: """Test block restored sleeping sensor missing last state.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry + hass, + SENSOR_DOMAIN, + "test_name_temperature", + "sensor_0-temp", + entry, + device_id=device.id, ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) @@ -293,9 +303,14 @@ async def test_block_not_matched_restored_sleeping_sensor( ) -> None: """Test block not matched to restored sleeping sensor.""" entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( - hass, SENSOR_DOMAIN, "test_name_temperature", "sensor_0-temp", entry + hass, + SENSOR_DOMAIN, + "test_name_temperature", + "sensor_0-temp", + entry, + device_id=device.id, ) extra_data = {"native_value": "20.4", "native_unit_of_measurement": "°C"} @@ -489,13 +504,14 @@ async def test_rpc_restored_sleeping_sensor( ) -> None: """Test RPC restored sensor.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, SENSOR_DOMAIN, "test_name_temperature", "temperature:0-temperature_0", entry, + device_id=device.id, ) extra_data = {"native_value": "21.0", "native_unit_of_measurement": "°C"} @@ -527,13 +543,14 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( ) -> None: """Test RPC restored sensor missing last state.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, SENSOR_DOMAIN, "test_name_temperature", "temperature:0-temperature_0", entry, + device_id=device.id, ) monkeypatch.setattr(mock_rpc_device, "initialized", False) diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 124562be8d5..c891d1d7b2d 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -118,13 +118,14 @@ async def test_block_restored_motion_switch( entry = await init_integration( hass, 1, sleep_period=1000, model=model, skip_setup=True ) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, SWITCH_DOMAIN, "test_name_motion_detection", "sensor_0-motionActive", entry, + device_id=device.id, ) mock_restore_cache(hass, [State(entity_id, STATE_OFF)]) @@ -154,13 +155,14 @@ async def test_block_restored_motion_switch_no_last_state( entry = await init_integration( hass, 1, sleep_period=1000, model=model, skip_setup=True ) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, SWITCH_DOMAIN, "test_name_motion_detection", "sensor_0-motionActive", entry, + device_id=device.id, ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 721e86559a3..c6434c0b988 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -385,13 +385,14 @@ async def test_rpc_restored_sleeping_update( ) -> None: """Test RPC restored update entity.""" entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, UPDATE_DOMAIN, "test_name_firmware_update", "sys-fwupdate", entry, + device_id=device.id, ) attr = {ATTR_INSTALLED_VERSION: "1", ATTR_LATEST_VERSION: "2"} @@ -443,13 +444,14 @@ async def test_rpc_restored_sleeping_update_no_last_state( }, ) entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) - register_device(device_registry, entry) + device = register_device(device_registry, entry) entity_id = register_entity( hass, UPDATE_DOMAIN, "test_name_firmware_update", "sys-fwupdate", entry, + device_id=device.id, ) monkeypatch.setattr(mock_rpc_device, "initialized", False)