From 46151456d80199a7785546464fea69f48a12cf2f Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:03:05 +0100 Subject: [PATCH] Make sure to clean register callbacks when mobile_app reloads (#156028) --- .../components/mobile_app/binary_sensor.py | 10 ++- homeassistant/components/mobile_app/sensor.py | 10 ++- .../mobile_app/test_binary_sensor.py | 88 ++++++++++++++++++- tests/components/mobile_app/test_sensor.py | 87 ++++++++++++++++++ 4 files changed, 186 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 8f8b8d97295..c8638aa37a3 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -61,10 +61,12 @@ async def async_setup_entry( async_add_entities([MobileAppBinarySensor(data, config_entry)]) - async_dispatcher_connect( - hass, - f"{DOMAIN}_{ENTITY_TYPE}_register", - handle_sensor_registration, + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{ENTITY_TYPE}_register", + handle_sensor_registration, + ) ) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 8200ad1fccd..6a2c55d2fd7 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -72,10 +72,12 @@ async def async_setup_entry( async_add_entities([MobileAppSensor(data, config_entry)]) - async_dispatcher_connect( - hass, - f"{DOMAIN}_{ENTITY_TYPE}_register", - handle_sensor_registration, + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{ENTITY_TYPE}_register", + handle_sensor_registration, + ) ) diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py index 9ffb61f92ab..d682cc3c5f3 100644 --- a/tests/components/mobile_app/test_binary_sensor.py +++ b/tests/components/mobile_app/test_binary_sensor.py @@ -6,9 +6,18 @@ from typing import Any from aiohttp.test_utils import TestClient import pytest -from homeassistant.const import STATE_UNKNOWN +from homeassistant.components.mobile_app.const import ( + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, + ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE, + ATTR_SENSOR_UNIQUE_ID, +) +from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send async def test_sensor( @@ -298,3 +307,80 @@ async def test_update_sensor_no_state( updated_entity = hass.states.get("binary_sensor.test_1_is_charging") assert updated_entity.state == STATE_UNKNOWN + + +async def test_dispatcher_cleanup_on_unload( + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, +) -> None: + """Test that dispatcher connections are cleaned up on config entry unload.""" + + webhook_id = create_registrations[1]["webhook_id"] + entry = hass.config_entries.async_entries("mobile_app")[1] + + # Send a dispatcher signal when config entry is loaded + async_dispatcher_send( + hass, + "mobile_app_binary_sensor_register", + { + CONF_WEBHOOK_ID: webhook_id, + ATTR_SENSOR_NAME: "Test Before Unload", + ATTR_SENSOR_STATE: True, + ATTR_SENSOR_TYPE: "binary_sensor", + ATTR_SENSOR_UNIQUE_ID: "test_before_unload", + ATTR_SENSOR_ICON: "mdi:test", + ATTR_SENSOR_ATTRIBUTES: {}, + }, + ) + await hass.async_block_till_done() + + # Check binary sensor was created + assert hass.states.get("binary_sensor.test_before_unload") is not None + + # Unload the config entry + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # Send another dispatcher signal after unload + async_dispatcher_send( + hass, + "mobile_app_binary_sensor_register", + { + CONF_WEBHOOK_ID: webhook_id, + ATTR_SENSOR_NAME: "Test After Unload", + ATTR_SENSOR_STATE: False, + ATTR_SENSOR_TYPE: "binary_sensor", + ATTR_SENSOR_UNIQUE_ID: "test_after_unload", + ATTR_SENSOR_ICON: "mdi:test", + ATTR_SENSOR_ATTRIBUTES: {}, + }, + ) + await hass.async_block_till_done() + + # The binary sensor should not be created because dispatcher was cleaned up + assert hass.states.get("binary_sensor.test_after_unload") is None + + # Reload the config entry + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Send dispatcher signal after reload + async_dispatcher_send( + hass, + "mobile_app_binary_sensor_register", + { + CONF_WEBHOOK_ID: webhook_id, + ATTR_SENSOR_NAME: "Test After Reload", + ATTR_SENSOR_STATE: True, + ATTR_SENSOR_TYPE: "binary_sensor", + ATTR_SENSOR_UNIQUE_ID: "test_after_reload", + ATTR_SENSOR_ICON: "mdi:test", + ATTR_SENSOR_ATTRIBUTES: {}, + }, + ) + await hass.async_block_till_done() + + # This binary sensor should be created successfully after reload + assert hass.states.get("binary_sensor.test_after_reload") is not None + assert hass.states.get("binary_sensor.test_after_reload").state == "on" diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index c12a8f6818b..b68a9496353 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -7,8 +7,17 @@ from unittest.mock import patch from aiohttp.test_utils import TestClient import pytest +from homeassistant.components.mobile_app.const import ( + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, + ATTR_SENSOR_STATE, + ATTR_SENSOR_TYPE, + ATTR_SENSOR_UNIQUE_ID, +) from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( + CONF_WEBHOOK_ID, PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -16,6 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.unit_system import ( METRIC_SYSTEM, US_CUSTOMARY_SYSTEM, @@ -697,3 +707,80 @@ async def test_recreate_correct_from_entity_registry( assert entity_entry.capabilities == { "state_class": "measurement", } + + +async def test_dispatcher_cleanup_on_unload( + hass: HomeAssistant, + create_registrations: tuple[dict[str, Any], dict[str, Any]], + webhook_client: TestClient, +) -> None: + """Test that dispatcher connections are cleaned up on config entry unload.""" + + webhook_id = create_registrations[1]["webhook_id"] + entry = hass.config_entries.async_entries("mobile_app")[1] + + # Send a dispatcher signal when config entry is loaded + async_dispatcher_send( + hass, + "mobile_app_sensor_register", + { + CONF_WEBHOOK_ID: webhook_id, + ATTR_SENSOR_NAME: "Test Before Unload", + ATTR_SENSOR_STATE: 42, + ATTR_SENSOR_TYPE: "sensor", + ATTR_SENSOR_UNIQUE_ID: "test_before_unload", + ATTR_SENSOR_ICON: "mdi:test", + ATTR_SENSOR_ATTRIBUTES: {}, + }, + ) + await hass.async_block_till_done() + + # Check sensor was created + assert hass.states.get("sensor.test_before_unload") is not None + + # Unload the config entry + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # Send another dispatcher signal after unload + async_dispatcher_send( + hass, + "mobile_app_sensor_register", + { + CONF_WEBHOOK_ID: webhook_id, + ATTR_SENSOR_NAME: "Test After Unload", + ATTR_SENSOR_STATE: 99, + ATTR_SENSOR_TYPE: "sensor", + ATTR_SENSOR_UNIQUE_ID: "test_after_unload", + ATTR_SENSOR_ICON: "mdi:test", + ATTR_SENSOR_ATTRIBUTES: {}, + }, + ) + await hass.async_block_till_done() + + # The sensor should not be created because dispatcher was cleaned up + assert hass.states.get("sensor.test_after_unload") is None + + # Reload the config entry + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Send dispatcher signal after reload + async_dispatcher_send( + hass, + "mobile_app_sensor_register", + { + CONF_WEBHOOK_ID: webhook_id, + ATTR_SENSOR_NAME: "Test After Reload", + ATTR_SENSOR_STATE: 123, + ATTR_SENSOR_TYPE: "sensor", + ATTR_SENSOR_UNIQUE_ID: "test_after_reload", + ATTR_SENSOR_ICON: "mdi:test", + ATTR_SENSOR_ATTRIBUTES: {}, + }, + ) + await hass.async_block_till_done() + + # This sensor should be created successfully after reload + assert hass.states.get("sensor.test_after_reload") is not None + assert hass.states.get("sensor.test_after_reload").state == "123"