From df8256735672abbe09d79b72787d55b96d5e14ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Aug 2024 08:14:45 -0500 Subject: [PATCH] Fix shelly available check when device is not initialized (#124182) * Fix shelly available check when device is not initialized available needs to check for device.initialized or if the device is sleepy as calls to status will raise NotInitialized which results in many unretrieved exceptions while writing state fixes ``` 2024-08-18 09:33:03.757 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved (None) Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 258, in _handle_refresh_interval await self._async_refresh(log_failures=True, scheduled=True) File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 453, in _async_refresh self.async_update_listeners() File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 168, in async_update_listeners update_callback() File "/config/custom_components/shelly/entity.py", line 374, in _update_callback self.async_write_ha_state() File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1005, in async_write_ha_state self._async_write_ha_state() File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1130, in _async_write_ha_state self.__async_calculate_state() File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1067, in __async_calculate_state state = self._stringify_state(available) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1011, in _stringify_state if (state := self.state) is None: ^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/components/binary_sensor/__init__.py", line 293, in state if (is_on := self.is_on) is None: ^^^^^^^^^^ File "/config/custom_components/shelly/binary_sensor.py", line 331, in is_on return bool(self.attribute_value) ^^^^^^^^^^^^^^^^^^^^ File "/config/custom_components/shelly/entity.py", line 545, in attribute_value self._last_value = self.sub_status ^^^^^^^^^^^^^^^ File "/config/custom_components/shelly/entity.py", line 534, in sub_status return self.status[self.entity_description.sub_key] ^^^^^^^^^^^ File "/config/custom_components/shelly/entity.py", line 364, in status return cast(dict, self.coordinator.device.status[self.key]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/aioshelly/rpc_device/device.py", line 390, in status raise NotInitialized aioshelly.exceptions.NotInitialized ``` * tweak * cover * fix * cover * fixes --- .../components/shelly/coordinator.py | 3 ++ homeassistant/components/shelly/entity.py | 8 +++++ tests/components/shelly/test_sensor.py | 34 ++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 2710565f960..6286e515727 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -682,6 +682,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self.entry.async_create_background_task( self.hass, self._async_connected(), "rpc device init", eager_start=True ) + # Make sure entities are marked available self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: self.entry.async_create_background_task( @@ -690,6 +691,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): "rpc device disconnected", eager_start=True, ) + # Make sure entities are marked as unavailable + self.async_set_updated_data(None) elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) if self.sleep_period: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 5bf8a411377..980a39feaba 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -358,6 +358,14 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) + @property + def available(self) -> bool: + """Check if device is available and initialized or sleepy.""" + coordinator = self.coordinator + return super().available and ( + coordinator.device.initialized or bool(coordinator.sleep_period) + ) + @property def status(self) -> dict: """Device status by entity key.""" diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 843270107e0..7aa4218341f 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -47,7 +47,7 @@ from . import ( register_entity, ) -from tests.common import mock_restore_cache_with_extra_data +from tests.common import async_fire_time_changed, mock_restore_cache_with_extra_data RELAY_BLOCK_ID = 0 SENSOR_BLOCK_ID = 3 @@ -1279,3 +1279,35 @@ async def test_rpc_rgbw_sensors( entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == f"123456789ABC-{light_type}:0-temperature_{light_type}" + + +async def test_rpc_device_sensor_goes_unavailable_on_disconnect( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC device with sensor goes unavailable on disconnect.""" + await init_integration(hass, 2) + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state is not None + assert temp_sensor_state.state != STATE_UNAVAILABLE + monkeypatch.setattr(mock_rpc_device, "connected", False) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + mock_rpc_device.mock_disconnected() + await hass.async_block_till_done() + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state.state == STATE_UNAVAILABLE + + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert "NotInitialized" not in caplog.text + + monkeypatch.setattr(mock_rpc_device, "connected", True) + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_initialized() + await hass.async_block_till_done() + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state.state != STATE_UNAVAILABLE