diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index b6e92301554..7e37c6ea7f2 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -12,7 +12,6 @@ from hatasmota.const import ( CONF_NAME, CONF_SW_VERSION, ) -from hatasmota.discovery import clear_discovery_topic from hatasmota.models import TasmotaDeviceConfig from hatasmota.mqtt import TasmotaMQTTClient @@ -23,11 +22,10 @@ from homeassistant.components.mqtt.subscription import ( async_unsubscribe_topics, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, - EVENT_DEVICE_REGISTRY_UPDATED, DeviceRegistry, async_entries_for_config_entry, ) @@ -77,42 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, mac, config, entry, tasmota_mqtt, device_registry ) - async def async_device_updated(event: Event) -> None: - """Handle the removal of a device.""" - device_registry = dr.async_get(hass) - device_id = event.data["device_id"] - if event.data["action"] not in ("remove", "update"): - return - - connections: set[tuple[str, str]] - if event.data["action"] == "update": - if "config_entries" not in event.data["changes"]: - return - - device = device_registry.async_get(device_id) - if not device: - # The device is already removed, do cleanup when we get "remove" event - return - if entry.entry_id in device.config_entries: - # Not removed from device - return - connections = device.connections - else: - deleted_device = device_registry.deleted_devices[event.data["device_id"]] - connections = deleted_device.connections - if entry.entry_id not in deleted_device.config_entries: - return - - macs = [c[1] for c in connections if c[0] == CONNECTION_NETWORK_MAC] - for mac in macs: - await clear_discovery_topic( - mac, entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt - ) - - hass.data[DATA_UNSUB].append( - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_updated) - ) - async def start_platforms() -> None: await device_automation.async_setup_entry(hass, entry) await asyncio.gather( @@ -165,7 +127,7 @@ async def _remove_device( tasmota_mqtt: TasmotaMQTTClient, device_registry: DeviceRegistry, ) -> None: - """Remove device from device registry.""" + """Remove a discovered Tasmota device.""" device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) if device is None or config_entry.entry_id not in device.config_entries: @@ -175,9 +137,6 @@ async def _remove_device( device_registry.async_update_device( device.id, remove_config_entry_id=config_entry.entry_id ) - await clear_discovery_topic( - mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt - ) def _update_device( @@ -218,5 +177,13 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove Tasmota config entry from a device.""" - # Just return True, cleanup is done on when handling device registry events + + connections = device_entry.connections + macs = [c[1] for c in connections if c[0] == CONNECTION_NETWORK_MAC] + tasmota_discovery = hass.data[discovery.TASMOTA_DISCOVERY_INSTANCE] + for mac in macs: + await tasmota_discovery.clear_discovery_topic( + mac, config_entry.data[CONF_DISCOVERY_PREFIX] + ) + return True diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a1f52517690..2f3a1b66fea 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.3.1"], + "requirements": ["hatasmota==0.4.0"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 000e5915840..a3f45f959b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -776,7 +776,7 @@ hass-nabucasa==0.54.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.3.1 +hatasmota==0.4.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e17eb48025d..7c846b837d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -535,7 +535,7 @@ hangups==0.4.17 hass-nabucasa==0.54.0 # homeassistant.components.tasmota -hatasmota==0.3.1 +hatasmota==0.4.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 6c296a78006..4744d6c2ccf 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -18,7 +18,7 @@ from hatasmota.utils import ( get_topic_tele_will, ) -from homeassistant.components.tasmota.const import DEFAULT_PREFIX +from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -97,6 +97,31 @@ DEFAULT_CONFIG_9_0_0_3 = { } +DEFAULT_SENSOR_CONFIG = { + "sn": { + "Time": "2020-09-25T12:47:15", + "DHT11": {"Temperature": None}, + "TempUnit": "C", + } +} + + +async def remove_device(hass, ws_client, device_id, config_entry_id=None): + """Remove config entry from a device.""" + if config_entry_id is None: + config_entry_id = hass.config_entries.async_entries(DOMAIN)[0].entry_id + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + assert response["success"] + + async def help_test_availability_when_connection_lost( hass, mqtt_client_mock, diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 24467ed5359..b5a59863bed 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -14,7 +14,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component -from .test_common import DEFAULT_CONFIG +from .test_common import DEFAULT_CONFIG, remove_device from tests.common import ( assert_lists_same, @@ -755,9 +755,10 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async def test_not_fires_on_mqtt_message_after_remove_from_registry( - hass, device_reg, calls, mqtt_mock, setup_tasmota + hass, hass_ws_client, device_reg, calls, mqtt_mock, setup_tasmota ): """Test triggers not firing after removal.""" + assert await async_setup_component(hass, "config", {}) # Discover a device with device trigger config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 @@ -803,7 +804,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( assert len(calls) == 1 # Remove the device - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() async_fire_mqtt_message( @@ -1037,9 +1038,10 @@ async def test_attach_remove_unknown1(hass, device_reg, mqtt_mock, setup_tasmota async def test_attach_unknown_remove_device_from_registry( - hass, device_reg, mqtt_mock, setup_tasmota + hass, hass_ws_client, device_reg, mqtt_mock, setup_tasmota ): """Test attach and removal of device with unknown trigger.""" + assert await async_setup_component(hass, "config", {}) # Discover a device without device triggers config1 = copy.deepcopy(DEFAULT_CONFIG) config1["swc"][0] = -1 @@ -1080,7 +1082,7 @@ async def test_attach_unknown_remove_device_from_registry( ) # Remove the device - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 90ca5d918fd..0b7e3726482 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -6,9 +6,10 @@ from unittest.mock import patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from .conftest import setup_tasmota_helper -from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3 +from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3, remove_device from tests.common import MockConfigEntry, async_fire_mqtt_message @@ -366,8 +367,11 @@ async def test_device_remove_multiple_config_entries_2( mqtt_mock.async_publish.assert_not_called() -async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_tasmota): +async def test_device_remove_stale( + hass, hass_ws_client, mqtt_mock, caplog, device_reg, setup_tasmota +): """Test removing a stale (undiscovered) device does not throw.""" + assert await async_setup_component(hass, "config", {}) mac = "00000049A3BC" config_entry = hass.config_entries.async_entries("tasmota")[0] @@ -385,7 +389,7 @@ async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_ta assert device_entry is not None # Remove the device - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) # Verify device entry is removed device_entry = device_reg.async_get_device( diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 7cbb12c49fc..6ad69592ae7 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -7,19 +7,29 @@ from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from .test_common import DEFAULT_CONFIG +from .test_common import DEFAULT_CONFIG, DEFAULT_SENSOR_CONFIG, remove_device -from tests.common import MockConfigEntry, async_fire_mqtt_message +from tests.common import ( + MockConfigEntry, + MockModule, + async_fire_mqtt_message, + mock_integration, +) async def test_device_remove( - hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota + hass, hass_ws_client, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota ): """Test removing a discovered device through device registry.""" + assert await async_setup_component(hass, "config", {}) config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) mac = config["mac"] async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + async_fire_mqtt_message( + hass, f"{DEFAULT_PREFIX}/{mac}/sensors", json.dumps(sensor_config) + ) await hass.async_block_till_done() # Verify device entry is created @@ -28,7 +38,7 @@ async def test_device_remove( ) assert device_entry is not None - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() # Verify device entry is removed @@ -51,7 +61,19 @@ async def test_device_remove_non_tasmota_device( hass, device_reg, hass_ws_client, mqtt_mock, setup_tasmota ): """Test removing a non Tasmota device through device registry.""" + assert await async_setup_component(hass, "config", {}) + + async def async_remove_config_entry_device(hass, config_entry, device_entry): + return True + + mock_integration( + hass, + MockModule( + "test", async_remove_config_entry_device=async_remove_config_entry_device + ), + ) config_entry = MockConfigEntry(domain="test") + config_entry.supports_remove_device = True config_entry.add_to_hass(hass) mac = "12:34:56:AB:CD:EF" @@ -61,7 +83,9 @@ async def test_device_remove_non_tasmota_device( ) assert device_entry is not None - device_reg.async_remove_device(device_entry.id) + await remove_device( + hass, await hass_ws_client(hass), device_entry.id, config_entry.entry_id + ) await hass.async_block_till_done() # Verify device entry is removed @@ -78,6 +102,7 @@ async def test_device_remove_stale_tasmota_device( hass, device_reg, hass_ws_client, mqtt_mock, setup_tasmota ): """Test removing a stale (undiscovered) Tasmota device through device registry.""" + assert await async_setup_component(hass, "config", {}) config_entry = hass.config_entries.async_entries("tasmota")[0] mac = "12:34:56:AB:CD:EF" @@ -87,7 +112,7 @@ async def test_device_remove_stale_tasmota_device( ) assert device_entry is not None - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() # Verify device entry is removed @@ -96,15 +121,8 @@ async def test_device_remove_stale_tasmota_device( ) assert device_entry is None - # Verify retained discovery topic has been cleared - mac = mac.replace(":", "") - mqtt_mock.async_publish.assert_has_calls( - [ - call(f"tasmota/discovery/{mac}/config", "", 0, True), - call(f"tasmota/discovery/{mac}/sensors", "", 0, True), - ], - any_order=True, - ) + # Verify retained discovery topic has not been cleared + mqtt_mock.async_publish.assert_not_called() async def test_tasmota_ws_remove_discovered_device( diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index e2f5f1111e1..8e810c82a43 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -22,6 +22,7 @@ from homeassistant.util import dt from .test_common import ( DEFAULT_CONFIG, + DEFAULT_SENSOR_CONFIG, help_test_availability, help_test_availability_discovery_update, help_test_availability_poll_state, @@ -35,14 +36,6 @@ from .test_common import ( from tests.common import async_fire_mqtt_message, async_fire_time_changed -DEFAULT_SENSOR_CONFIG = { - "sn": { - "Time": "2020-09-25T12:47:15", - "DHT11": {"Temperature": None}, - "TempUnit": "C", - } -} - BAD_INDEXED_SENSOR_CONFIG_3 = { "sn": { "Time": "2020-09-25T12:47:15",