From ed1f3eb4ab861ab9c94e6275304b002d9acdde9d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Nov 2024 08:20:04 +1300 Subject: [PATCH 1/9] Do not create ESPHome Dashboard update entity if no configuration found --- homeassistant/components/esphome/update.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 5e571399ecb..8ad6113d6a1 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -58,9 +58,11 @@ async def async_setup_entry( state_type=UpdateState, ) - if (dashboard := async_get_dashboard(hass)) is None: - return entry_data = DomainData.get(hass).get_entry_data(entry) + if ((dashboard := async_get_dashboard(hass)) is None) or ( + dashboard.data.get(entry_data.device_info.name) is None + ): + return unsubs: list[CALLBACK_TYPE] = [] @callback From 522d98574e630c628bc750f530beec5d24f132ff Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:42:42 +1300 Subject: [PATCH 2/9] assert not none --- homeassistant/components/esphome/update.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 8ad6113d6a1..aabba5b2e24 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -59,6 +59,7 @@ async def async_setup_entry( ) entry_data = DomainData.get(hass).get_entry_data(entry) + assert entry_data.device_info is not None if ((dashboard := async_get_dashboard(hass)) is None) or ( dashboard.data.get(entry_data.device_info.name) is None ): From a7a42419063a30f4e145b6955464ddee72d999d6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:57:42 +1300 Subject: [PATCH 3/9] Check device info for not fully connected yet --- homeassistant/components/esphome/update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index aabba5b2e24..833f29621e4 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -59,7 +59,9 @@ async def async_setup_entry( ) entry_data = DomainData.get(hass).get_entry_data(entry) - assert entry_data.device_info is not None + # If the device_info is not available yet, the connection is not fully established + if entry_data.device_info is None: + return if ((dashboard := async_get_dashboard(hass)) is None) or ( dashboard.data.get(entry_data.device_info.name) is None ): From 2d3004d3b6c94fb173e6feb48cf87b09ea775c23 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:21:41 +1300 Subject: [PATCH 4/9] Check for device config in different position to allow creating the entity if config comes later. --- homeassistant/components/esphome/update.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 833f29621e4..ec17beebbaa 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -58,14 +58,11 @@ async def async_setup_entry( state_type=UpdateState, ) + if (dashboard := async_get_dashboard(hass)) is None: + return entry_data = DomainData.get(hass).get_entry_data(entry) - # If the device_info is not available yet, the connection is not fully established - if entry_data.device_info is None: - return - if ((dashboard := async_get_dashboard(hass)) is None) or ( - dashboard.data.get(entry_data.device_info.name) is None - ): - return + assert entry_data.device_info is not None + device_name = entry_data.device_info.name unsubs: list[CALLBACK_TYPE] = [] @callback @@ -77,13 +74,21 @@ async def async_setup_entry( if not entry_data.available or not dashboard.last_update_success: return + # Do not add Dashboard Entity if this device is not known to the ESPHome dashboard. + if dashboard.data.get(device_name) is None: + return + for unsub in unsubs: unsub() unsubs.clear() async_add_entities([ESPHomeDashboardUpdateEntity(entry_data, dashboard)]) - if entry_data.available and dashboard.last_update_success: + if ( + entry_data.available + and dashboard.last_update_success + and dashboard.data.get(device_name) + ): _async_setup_update_entity() return From 0b07f574e916bd0c2e2aee1389e58dd953e52cc2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:22:43 +1300 Subject: [PATCH 5/9] Add new test --- tests/components/esphome/test_update.py | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 7593ab21838..58ccbc0f652 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -433,6 +433,35 @@ async def test_update_becomes_available_at_runtime( assert features is UpdateEntityFeature.INSTALL +async def test_update_entity_not_present_with_dashboard_but_unknown_device( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + mock_dashboard: dict[str, Any], +) -> None: + """Test ESPHome update entity does not get created if the device is unknown to the dashboard.""" + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + ) + + mock_dashboard["configured"] = [] + + state = hass.states.get("update.test_firmware") + assert state is None + + await async_get_dashboard(hass).async_refresh() + await hass.async_block_till_done() + + state = hass.states.get("update.none_firmware") + assert state is None + + async def test_generic_device_update_entity( hass: HomeAssistant, mock_client: APIClient, From 72d7788cf6658be0219cc6dabf641326e0f41aee Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:23:22 +1300 Subject: [PATCH 6/9] Update existing tests to account for entity not being created now instead of being unavailable --- tests/components/esphome/test_update.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 58ccbc0f652..cc29a58c854 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -31,7 +31,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -83,11 +82,6 @@ def stub_reconnect(): "supported_features": 0, }, ), - ( - [], - STATE_UNKNOWN, # dashboard is available but device is unknown - {"supported_features": 0}, - ), ], ) async def test_update_entity( @@ -408,11 +402,7 @@ async def test_update_becomes_available_at_runtime( ) await hass.async_block_till_done() state = hass.states.get("update.test_firmware") - assert state is not None - features = state.attributes[ATTR_SUPPORTED_FEATURES] - # There are no devices on the dashboard so no - # way to tell the version so install is disabled - assert features is UpdateEntityFeature(0) + assert state is None # A device gets added to the dashboard mock_dashboard["configured"] = [ From ef17733a6a9a3a514a4d28fdc2d260eef2b30c59 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 Nov 2024 06:51:23 +1300 Subject: [PATCH 7/9] Check data is not none too --- homeassistant/components/esphome/update.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index ec17beebbaa..86802e1ebe6 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -75,7 +75,7 @@ async def async_setup_entry( return # Do not add Dashboard Entity if this device is not known to the ESPHome dashboard. - if dashboard.data.get(device_name) is None: + if dashboard.data is None or dashboard.data.get(device_name) is None: return for unsub in unsubs: @@ -87,6 +87,7 @@ async def async_setup_entry( if ( entry_data.available and dashboard.last_update_success + and dashboard.data is not None and dashboard.data.get(device_name) ): _async_setup_update_entity() From 35c5e598cb84b00cb481dfb32437ddc2323165b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 Nov 2024 07:07:50 +1300 Subject: [PATCH 8/9] Add dummy device --- tests/components/esphome/test_update.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index cc29a58c854..5060471f5d2 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -440,7 +440,13 @@ async def test_update_entity_not_present_with_dashboard_but_unknown_device( states=[], ) - mock_dashboard["configured"] = [] + mock_dashboard["configured"] = [ + { + "name": "other-test", + "current_version": "2023.2.0-dev", + "configuration": "other-test.yaml", + } + ] state = hass.states.get("update.test_firmware") assert state is None From a8d3bf8be847b4608332d77182da06151058ec54 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 Nov 2024 07:08:22 +1300 Subject: [PATCH 9/9] `device` is always present at this stage now --- homeassistant/components/esphome/update.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 86802e1ebe6..2b593051742 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -144,10 +144,8 @@ class ESPHomeDashboardUpdateEntity( self._attr_supported_features = NO_FEATURES self._attr_installed_version = device_info.esphome_version device = coordinator.data.get(device_info.name) - if device is None: - self._attr_latest_version = None - else: - self._attr_latest_version = device["current_version"] + assert device is not None + self._attr_latest_version = device["current_version"] @callback def _handle_coordinator_update(self) -> None: