1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 04:50:05 +00:00

Improve Plugwise coordinator code (#158983)

This commit is contained in:
Bouwe Westerdijk
2025-12-18 18:00:42 +01:00
committed by GitHub
parent f40f7072c8
commit c449b2e2e8
3 changed files with 91 additions and 28 deletions

View File

@@ -7,6 +7,7 @@ from plugwise import GwEntityData, Smile
from plugwise.exceptions import ( from plugwise.exceptions import (
ConnectionFailedError, ConnectionFailedError,
InvalidAuthentication, InvalidAuthentication,
InvalidSetupError,
InvalidXMLError, InvalidXMLError,
PlugwiseError, PlugwiseError,
ResponseError, ResponseError,
@@ -31,6 +32,9 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData
"""Class to manage fetching Plugwise data from single endpoint.""" """Class to manage fetching Plugwise data from single endpoint."""
_connected: bool = False _connected: bool = False
_current_devices: set[str]
_stored_devices: set[str]
new_devices: set[str]
config_entry: PlugwiseConfigEntry config_entry: PlugwiseConfigEntry
@@ -59,14 +63,31 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData
port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT), port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
websession=async_get_clientsession(hass, verify_ssl=False), websession=async_get_clientsession(hass, verify_ssl=False),
) )
self._current_devices: set[str] = set() self._current_devices = set()
self.new_devices: set[str] = set() self._stored_devices = set()
self.new_devices = set()
async def _connect(self) -> None: async def _connect(self) -> None:
"""Connect to the Plugwise Smile.""" """Connect to the Plugwise Smile.
A Version object is received when the connection succeeds.
"""
version = await self.api.connect() version = await self.api.connect()
self._connected = isinstance(version, Version) self._connected = isinstance(version, Version)
async def _async_setup(self) -> None:
"""Initialize the update_data process."""
device_reg = dr.async_get(self.hass)
device_entries = dr.async_entries_for_config_entry(
device_reg, self.config_entry.entry_id
)
self._stored_devices = {
identifier[1]
for device_entry in device_entries
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
}
async def _async_update_data(self) -> dict[str, GwEntityData]: async def _async_update_data(self) -> dict[str, GwEntityData]:
"""Fetch data from Plugwise.""" """Fetch data from Plugwise."""
try: try:
@@ -83,10 +104,15 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="authentication_failed", translation_key="authentication_failed",
) from err ) from err
except InvalidSetupError as err:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="invalid_setup",
) from err
except (InvalidXMLError, ResponseError) as err: except (InvalidXMLError, ResponseError) as err:
raise UpdateFailed( raise UpdateFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="invalid_xml_data", translation_key="response_error",
) from err ) from err
except PlugwiseError as err: except PlugwiseError as err:
raise UpdateFailed( raise UpdateFailed(
@@ -104,12 +130,16 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData
def _async_add_remove_devices(self, data: dict[str, GwEntityData]) -> None: def _async_add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
"""Add new Plugwise devices, remove non-existing devices.""" """Add new Plugwise devices, remove non-existing devices."""
# Check for new or removed devices set_of_data = set(data)
self.new_devices = set(data) - self._current_devices # Check for new or removed devices,
removed_devices = self._current_devices - set(data) # 'new_devices' contains all devices present in 'data' at init ('self._current_devices' is empty)
self._current_devices = set(data) # this is required for the proper initialization of all the present platform entities.
self.new_devices = set_of_data - self._current_devices
if removed_devices: current_devices = (
self._stored_devices if not self._current_devices else self._current_devices
)
self._current_devices = set_of_data
if current_devices - set_of_data: # device(s) to remove
self._async_remove_devices(data) self._async_remove_devices(data)
def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None: def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None:
@@ -118,17 +148,17 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData
device_list = dr.async_entries_for_config_entry( device_list = dr.async_entries_for_config_entry(
device_reg, self.config_entry.entry_id device_reg, self.config_entry.entry_id
) )
# First find the Plugwise via_device # First find the Plugwise via_device
gateway_device = device_reg.async_get_device({(DOMAIN, self.api.gateway_id)}) gateway_device = device_reg.async_get_device({(DOMAIN, self.api.gateway_id)})
assert gateway_device is not None assert gateway_device is not None
via_device_id = gateway_device.id via_device_id = gateway_device.id
# Then remove the connected orphaned device(s) # Then remove the connected orphaned device(s)
for device_entry in device_list: for device_entry in device_list:
for identifier in device_entry.identifiers: for identifier in device_entry.identifiers:
if identifier[0] == DOMAIN:
if ( if (
device_entry.via_device_id == via_device_id identifier[0] == DOMAIN
and device_entry.via_device_id == via_device_id
and identifier[1] not in data and identifier[1] not in data
): ):
device_reg.async_update_device( device_reg.async_update_device(
@@ -136,7 +166,7 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData
remove_config_entry_id=self.config_entry.entry_id, remove_config_entry_id=self.config_entry.entry_id,
) )
LOGGER.debug( LOGGER.debug(
"Removed %s device %s %s from device_registry", "Removed %s device/zone %s %s from device_registry",
DOMAIN, DOMAIN,
device_entry.model, device_entry.model,
identifier[1], identifier[1],

View File

@@ -319,7 +319,10 @@
"failed_to_connect": { "failed_to_connect": {
"message": "[%key:common::config_flow::error::cannot_connect%]" "message": "[%key:common::config_flow::error::cannot_connect%]"
}, },
"invalid_xml_data": { "invalid_setup": {
"message": "Add your Adam instead of your Anna, see the documentation"
},
"response_error": {
"message": "[%key:component::plugwise::config::error::response_error%]" "message": "[%key:component::plugwise::config::error::response_error%]"
}, },
"set_schedule_first": { "set_schedule_first": {

View File

@@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory
from plugwise.exceptions import ( from plugwise.exceptions import (
ConnectionFailedError, ConnectionFailedError,
InvalidAuthentication, InvalidAuthentication,
InvalidSetupError,
InvalidXMLError, InvalidXMLError,
PlugwiseError, PlugwiseError,
ResponseError, ResponseError,
@@ -89,6 +90,7 @@ async def test_load_unload_config_entry(
[ [
(ConnectionFailedError, ConfigEntryState.SETUP_RETRY), (ConnectionFailedError, ConfigEntryState.SETUP_RETRY),
(InvalidAuthentication, ConfigEntryState.SETUP_ERROR), (InvalidAuthentication, ConfigEntryState.SETUP_ERROR),
(InvalidSetupError, ConfigEntryState.SETUP_ERROR),
(InvalidXMLError, ConfigEntryState.SETUP_RETRY), (InvalidXMLError, ConfigEntryState.SETUP_RETRY),
(PlugwiseError, ConfigEntryState.SETUP_RETRY), (PlugwiseError, ConfigEntryState.SETUP_RETRY),
(ResponseError, ConfigEntryState.SETUP_RETRY), (ResponseError, ConfigEntryState.SETUP_RETRY),
@@ -169,7 +171,7 @@ async def test_migrate_unique_id_temperature(
"""Test migration of unique_id.""" """Test migration of unique_id."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
entity: entity_registry.RegistryEntry = entity_registry.async_get_or_create( entity: er.RegistryEntry = entity_registry.async_get_or_create(
**entitydata, **entitydata,
config_entry=mock_config_entry, config_entry=mock_config_entry,
) )
@@ -334,3 +336,31 @@ async def test_update_device(
for device_entry in list(device_registry.devices.values()): for device_entry in list(device_registry.devices.values()):
item_list.extend(x[1] for x in device_entry.identifiers) item_list.extend(x[1] for x in device_entry.identifiers)
assert "1772a4ea304041adb83f357b751341ff" not in item_list assert "1772a4ea304041adb83f357b751341ff" not in item_list
@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True)
@pytest.mark.parametrize("cooling_present", [False], indirect=True)
async def test_delete_removed_device(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_smile_adam_heat_cool: MagicMock,
device_registry: dr.DeviceRegistry,
init_integration: MockConfigEntry,
) -> None:
"""Test device removal at integration init."""
data = mock_smile_adam_heat_cool.async_update.return_value
item_list: list[str] = []
for device_entry in device_registry.devices.values():
item_list.extend(x[1] for x in device_entry.identifiers)
assert "14df5c4dc8cb4ba69f9d1ac0eaf7c5c6" in item_list
data.pop("14df5c4dc8cb4ba69f9d1ac0eaf7c5c6")
with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data):
await hass.config_entries.async_reload(init_integration.entry_id)
await hass.async_block_till_done()
item_list = []
for device_entry in device_registry.devices.values():
item_list.extend(x[1] for x in device_entry.identifiers)
assert "14df5c4dc8cb4ba69f9d1ac0eaf7c5c6" not in item_list