diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d246590..59e6fac69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Versions from 0.40 and up +## v0.58.1 + +- Repair automatic device-removal via PR [#940](https://github.com/plugwise/plugwise-beta/pull/940) +- Improve detection of deleted devices via plugwise [v1.8.1](https://github.com/plugwise/python-plugwise/releases/tag/v1.8.1) + ## v0.58.0 - Fix mypy errors in Core (not accepted in HA Core) diff --git a/custom_components/plugwise/__init__.py b/custom_components/plugwise/__init__.py index 3d509da98..0b7075f51 100644 --- a/custom_components/plugwise/__init__.py +++ b/custom_components/plugwise/__init__.py @@ -146,7 +146,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) - """Migrate back to v1.1 config entry.""" if entry.version > 1: # This means the user has downgraded from a future version - return False + return False # pragma: no cover if entry.version == 1 and entry.minor_version == 2: new_data = {**entry.data} diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index aa008f66a..9e43634c5 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -203,7 +203,7 @@ def hvac_mode(self) -> HVACMode: return HVACMode.HEAT # pragma: no cover try: hvac = HVACMode(mode) - except ValueError: + except ValueError: # pragma: no cover return HVACMode.HEAT # pragma: no cover if hvac not in self.hvac_modes: return HVACMode.HEAT # pragma: no cover diff --git a/custom_components/plugwise/coordinator.py b/custom_components/plugwise/coordinator.py index ec17d4e02..90e0f339a 100644 --- a/custom_components/plugwise/coordinator.py +++ b/custom_components/plugwise/coordinator.py @@ -25,6 +25,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.device_registry import DeviceEntry, DeviceRegistry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from packaging.version import Version @@ -73,7 +74,7 @@ def __init__( username=self.config_entry.data[CONF_USERNAME], websession=async_get_clientsession(hass, verify_ssl=False), ) - self._current_devices: set[str] = set() + self._current_devices: list[DeviceEntry] = [] self.new_devices: set[str] = set() self.update_interval = update_interval @@ -132,34 +133,43 @@ async def _async_update_data(self) -> dict[str, GwEntityData]: async def _async_add_remove_devices(self, data: dict[str, GwEntityData]) -> None: """Add new Plugwise devices, remove non-existing devices.""" # Check for new or removed devices - self.new_devices = set(data) - self._current_devices - removed_devices = self._current_devices - set(data) - self._current_devices = set(data) - - if removed_devices: - await self._async_remove_devices(data) - - async def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None: - """Clean registries when removed devices found.""" - device_reg = dr.async_get(self.hass) - device_list = dr.async_entries_for_config_entry( - device_reg, self.config_entry.entry_id + device_registry = dr.async_get(self.hass) + self._current_devices = dr.async_entries_for_config_entry( + device_registry, self.config_entry.entry_id ) - + current_device_ids = { + identifier[1] + for device_entry in self._current_devices + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + } + data_ids = set(data) + self.new_devices = data_ids - current_device_ids + removed_ids: set[str] = current_device_ids - data_ids + + if removed_ids: + await self._async_remove_devices(device_registry, removed_ids) + + async def _async_remove_devices( + self, + device_registry: DeviceRegistry, + removed_ids: set[str], + ) -> None: + """Clean registries when removed devices found.""" # First find the Plugwise via_device - gateway_device = device_reg.async_get_device({(DOMAIN, self.api.gateway_id)}) - if gateway_device is not None: - via_device_id = gateway_device.id + gateway_device = device_registry.async_get_device({(DOMAIN, self.api.gateway_id)}) + if gateway_device is None: + LOGGER.warning("Failed to remove device, plugwise gateway reference not found") # pragma: no cover + return # pragma: no cover # Then remove the connected orphaned device(s) - for device_entry in device_list: + via_device_id = gateway_device.id + for device_entry in self._current_devices: + if device_entry.via_device_id != via_device_id: + continue for identifier in device_entry.identifiers: - if ( - identifier[0] == DOMAIN - and device_entry.via_device_id == via_device_id - and identifier[1] not in data - ): - device_reg.async_update_device( + if identifier[0] == DOMAIN and identifier[1] in removed_ids: + device_registry.async_update_device( device_entry.id, remove_config_entry_id=self.config_entry.entry_id ) LOGGER.debug( @@ -168,3 +178,4 @@ async def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None: device_entry.model, identifier[1], ) + break diff --git a/custom_components/plugwise/manifest.json b/custom_components/plugwise/manifest.json index cfd68486a..0bb4ddedf 100644 --- a/custom_components/plugwise/manifest.json +++ b/custom_components/plugwise/manifest.json @@ -7,7 +7,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["plugwise"], - "requirements": ["plugwise==1.8.0"], - "version": "0.58.0", + "requirements": ["plugwise==1.8.1"], + "version": "0.58.1", "zeroconf": ["_plugwise._tcp.local."] } diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index d587a04af..f49a20d5e 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -139,20 +139,19 @@ async def test_p1_3ph_dsmr_sensor_disabled_entities( mock_smile_p1: MagicMock, init_integration: MockConfigEntry, ) -> None: - """Test disabled power related sensor entities intent.""" + """Test enabling of disabled voltage sensor.""" entity_id = "sensor.p1_voltage_phase_one" - state = hass.states.get(entity_id) - assert not state - - entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) - await hass.async_block_till_done() - - await hass.config_entries.async_reload(init_integration.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.p1_voltage_phase_one") - assert state - assert float(state.state) == 233.2 + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "b82b6b3322484f2ea4e25e0bd5f3d61f-voltage_phase_one" + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + updated_entry = entity_registry.async_update_entity( + entity_id=entity_id, disabled_by=None + ) + assert updated_entry != entry + assert updated_entry.disabled is False @pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)])