From be468db221f1fa529d358ecd2eb542f015bf49dc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Aug 2026 15:51:54 +0200 Subject: [PATCH] Drop ecobee thermostats from the action list on unload (#180399) --- homeassistant/components/ecobee/climate.py | 15 ++++++++-- tests/components/ecobee/test_climate.py | 32 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index ec7bde1e9ef8..fdc0009cddbb 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -27,7 +27,7 @@ from homeassistant.const import ( STATE_ON, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import ( config_validation as cv, @@ -168,7 +168,18 @@ async def async_setup_entry( entities.append(Thermostat(data, index, thermostat, hass)) async_add_entities(entities, True) - _async_get_thermostats(hass).extend(entities) + + # The ecobee actions act on whatever is in this list, so the entities have + # to be taken back out again when the entry goes away. + thermostats = _async_get_thermostats(hass) + thermostats.extend(entities) + + @callback + def _remove_thermostats() -> None: + for entity in entities: + thermostats.remove(entity) + + config_entry.async_on_unload(_remove_thermostats) platform = entity_platform.async_get_current_platform() diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index ff8fe064d065..83be62f31e05 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -2,6 +2,7 @@ from http import HTTPStatus from unittest import mock +from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -15,6 +16,7 @@ from homeassistant.components.ecobee.climate import ( Thermostat, ) from homeassistant.components.ecobee.const import DOMAIN +from homeassistant.components.ecobee.services import _async_get_thermostats from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError @@ -700,3 +702,33 @@ async def test_set_sensors_used_in_climate(hass: HomeAssistant) -> None: ) assert execinfo.value.translation_domain == "ecobee" assert execinfo.value.translation_key == "sensor_lookup_failed" + + +async def test_thermostats_removed_from_services_on_unload( + hass: HomeAssistant, +) -> None: + """Test unloading drops the entities the ecobee actions iterate over.""" + entry = await setup_platform(hass, [const.Platform.CLIMATE]) + + assert _async_get_thermostats(hass) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # The actions call methods on whatever is in this list, so entities that + # are gone must not be left behind for them to find. + assert not _async_get_thermostats(hass) + + +async def test_thermostats_not_duplicated_on_reload(hass: HomeAssistant) -> None: + """Test reloading does not leave a second copy of every entity behind.""" + entry = await setup_platform(hass, [const.Platform.CLIMATE]) + + before = len(_async_get_thermostats(hass)) + assert before + + with patch("homeassistant.components.ecobee.PLATFORMS", [const.Platform.CLIMATE]): + assert await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert len(_async_get_thermostats(hass)) == before