From ca93624ec6e5d800645b950c6f9f85bdff86c303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Mon, 12 May 2025 20:47:26 +0000 Subject: [PATCH 1/4] Remove caching of device_info in `ESPHome` --- homeassistant/components/esphome/entity.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 8eded610194794..6bbfd6153adc70 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -239,7 +239,6 @@ def __init__( self._states = cast(dict[int, _StateT], entry_data.state[state_type]) assert entry_data.device_info is not None device_info = entry_data.device_info - self._device_info = device_info self._on_entry_data_changed() self._key = entity_info.key self._state_type = state_type @@ -278,6 +277,12 @@ async def async_added_to_hass(self) -> None: ) self._update_state_from_entry_data() + @property + def _device_info(self) -> EsphomeDeviceInfo: + """Return the device info.""" + assert self._entry_data.device_info is not None + return self._entry_data.device_info + @callback def _on_static_info_update(self, static_info: EntityInfo) -> None: """Save the static info for this entity when it changes. From 5c1c5279f4cb5c1f560939206fc735f46f2460c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 10:01:58 -0500 Subject: [PATCH 2/4] tweaks --- homeassistant/components/esphome/entity.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 6bbfd6153adc70..6d3f2663a50e2b 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -277,12 +277,6 @@ async def async_added_to_hass(self) -> None: ) self._update_state_from_entry_data() - @property - def _device_info(self) -> EsphomeDeviceInfo: - """Return the device info.""" - assert self._entry_data.device_info is not None - return self._entry_data.device_info - @callback def _on_static_info_update(self, static_info: EntityInfo) -> None: """Save the static info for this entity when it changes. @@ -290,12 +284,13 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None: This method can be overridden in child classes to know when the static info changes. """ - device_info = self._entry_data.device_info if TYPE_CHECKING: static_info = cast(_InfoT, static_info) - assert device_info + assert self._device_info self._static_info = static_info - self._attr_unique_id = build_unique_id(device_info.mac_address, static_info) + self._attr_unique_id = build_unique_id( + self._device_info.mac_address, static_info + ) self._attr_entity_registry_enabled_default = not static_info.disabled_by_default # https://github.com/home-assistant/core/issues/132532 # If the name is "", we need to set it to None since otherwise @@ -332,6 +327,11 @@ def _on_state_update(self) -> None: @callback def _on_entry_data_changed(self) -> None: entry_data = self._entry_data + # Update the device info since it can change + # when the device is reconnected + if TYPE_CHECKING: + assert entry_data.device_info is not None + self._device_info = entry_data.device_info self._api_version = entry_data.api_version self._client = entry_data.client if self._device_info.has_deep_sleep: From c2484f3d6fe77133bad19d36fc87863ce21f8f36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 10:05:08 -0500 Subject: [PATCH 3/4] revert unneeded change --- homeassistant/components/esphome/entity.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 6d3f2663a50e2b..15ea54422d4166 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -284,13 +284,12 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None: This method can be overridden in child classes to know when the static info changes. """ + device_info = self._entry_data.device_info if TYPE_CHECKING: static_info = cast(_InfoT, static_info) - assert self._device_info + assert device_info self._static_info = static_info - self._attr_unique_id = build_unique_id( - self._device_info.mac_address, static_info - ) + self._attr_unique_id = build_unique_id(device_info.mac_address, static_info) self._attr_entity_registry_enabled_default = not static_info.disabled_by_default # https://github.com/home-assistant/core/issues/132532 # If the name is "", we need to set it to None since otherwise From 4d853c3cecab560e4d738d4f604e38fab91a34c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 May 2025 12:03:46 -0500 Subject: [PATCH 4/4] coverage --- tests/components/esphome/test_entity.py | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index ee6e6b6785fcd5..36185efeb72d9b 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -1,6 +1,7 @@ """Test ESPHome binary sensors.""" import asyncio +from dataclasses import asdict from typing import Any from unittest.mock import AsyncMock @@ -8,6 +9,7 @@ APIClient, BinarySensorInfo, BinarySensorState, + DeviceInfo, SensorInfo, SensorState, build_unique_id, @@ -665,3 +667,63 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage( ) state = hass.states.get("binary_sensor.user_named") assert state is not None + + +async def test_deep_sleep_added_after_setup( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test deep sleep added after setup.""" + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[ + BinarySensorInfo( + object_id="test", + key=1, + name="test", + unique_id="test", + ), + ], + user_service=[], + states=[ + BinarySensorState(key=1, state=True, missing_state=False), + ], + device_info={"has_deep_sleep": False}, + ) + + entity_id = "binary_sensor.test_test" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(expected_disconnect=True) + + # No deep sleep, should be unavailable + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await mock_device.mock_connect() + + # reconnect, should be available + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(expected_disconnect=True) + new_device_info = DeviceInfo( + **{**asdict(mock_device.device_info), "has_deep_sleep": True} + ) + mock_device.client.device_info = AsyncMock(return_value=new_device_info) + mock_device.device_info = new_device_info + + await mock_device.mock_connect() + + # Now disconnect that deep sleep is set in device info + await mock_device.mock_disconnect(expected_disconnect=True) + + # Deep sleep, should be available + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON