From f028d1a1cae05598bdb9b7c4cd156d9f8af510e5 Mon Sep 17 00:00:00 2001 From: Aaron Collins Date: Thu, 6 Jul 2023 00:12:18 +1200 Subject: [PATCH] Bump pydaikin 2.10.5 (#95656) --- .coveragerc | 1 - homeassistant/components/daikin/__init__.py | 86 +++++++++++- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/daikin/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/daikin/test_init.py | 128 ++++++++++++++++++ 7 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 tests/components/daikin/test_init.py diff --git a/.coveragerc b/.coveragerc index 75402c71325..f2092abef63 100644 --- a/.coveragerc +++ b/.coveragerc @@ -182,7 +182,6 @@ omit = homeassistant/components/crownstone/listeners.py homeassistant/components/cups/sensor.py homeassistant/components/currencylayer/sensor.py - homeassistant/components/daikin/__init__.py homeassistant/components/daikin/climate.py homeassistant/components/daikin/sensor.py homeassistant/components/daikin/switch.py diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 481a072bdb3..b0097f607d5 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -15,8 +15,9 @@ from homeassistant.const import ( CONF_UUID, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -52,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not daikin_api: return False + await async_migrate_unique_id(hass, entry, daikin_api) + hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -67,7 +70,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def daikin_api_setup(hass, host, key, uuid, password): +async def daikin_api_setup(hass: HomeAssistant, host, key, uuid, password): """Create a Daikin instance only once.""" session = async_get_clientsession(hass) @@ -127,3 +130,82 @@ class DaikinApi: name=info.get("name"), sw_version=info.get("ver", "").replace("_", "."), ) + + +async def async_migrate_unique_id( + hass: HomeAssistant, config_entry: ConfigEntry, api: DaikinApi +) -> None: + """Migrate old entry.""" + dev_reg = dr.async_get(hass) + old_unique_id = config_entry.unique_id + new_unique_id = api.device.mac + new_name = api.device.values["name"] + + @callback + def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: + """Update unique ID of entity entry.""" + return update_unique_id(entity_entry, new_unique_id) + + if new_unique_id == old_unique_id: + return + + # Migrate devices + for device_entry in dr.async_entries_for_config_entry( + dev_reg, config_entry.entry_id + ): + for connection in device_entry.connections: + if connection[1] == old_unique_id: + new_connections = { + (CONNECTION_NETWORK_MAC, dr.format_mac(new_unique_id)) + } + + _LOGGER.debug( + "Migrating device %s connections to %s", + device_entry.name, + new_connections, + ) + dev_reg.async_update_device( + device_entry.id, + merge_connections=new_connections, + ) + + if device_entry.name is None: + _LOGGER.debug( + "Migrating device name to %s", + new_name, + ) + dev_reg.async_update_device( + device_entry.id, + name=new_name, + ) + + # Migrate entities + await er.async_migrate_entries(hass, config_entry.entry_id, _update_unique_id) + + new_data = {**config_entry.data, KEY_MAC: dr.format_mac(new_unique_id)} + + hass.config_entries.async_update_entry( + config_entry, unique_id=new_unique_id, data=new_data + ) + + +@callback +def update_unique_id( + entity_entry: er.RegistryEntry, unique_id: str +) -> dict[str, str] | None: + """Update unique ID of entity entry.""" + if entity_entry.unique_id.startswith(unique_id): + # Already correct, nothing to do + return None + + unique_id_parts = entity_entry.unique_id.split("-") + unique_id_parts[0] = unique_id + entity_new_unique_id = "-".join(unique_id_parts) + + _LOGGER.debug( + "Migrating entity %s from %s to new id %s", + entity_entry.entity_id, + entity_entry.unique_id, + entity_new_unique_id, + ) + return {"new_unique_id": entity_new_unique_id} diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 6f90b0cf5ef..c6334dfaeca 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["pydaikin"], "quality_scale": "platinum", - "requirements": ["pydaikin==2.9.0"], + "requirements": ["pydaikin==2.10.5"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 37b3ec45c4c..847f030fae5 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -42,7 +42,7 @@ async def async_setup_entry( [ DaikinZoneSwitch(daikin_api, zone_id) for zone_id, zone in enumerate(zones) - if zone != ("-", "0") + if zone[0] != "-" ] ) if daikin_api.device.support_advanced_modes: diff --git a/requirements_all.txt b/requirements_all.txt index 5e99d5774ce..5f32008df46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1618,7 +1618,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.9.0 +pydaikin==2.10.5 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dccc608c1aa..3585613fe0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1200,7 +1200,7 @@ pycoolmasternet-async==0.1.5 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.9.0 +pydaikin==2.10.5 # homeassistant.components.deconz pydeconz==113 diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py new file mode 100644 index 00000000000..8145a7a1e99 --- /dev/null +++ b/tests/components/daikin/test_init.py @@ -0,0 +1,128 @@ +"""Define tests for the Daikin init.""" +import asyncio +from unittest.mock import AsyncMock, PropertyMock, patch + +from aiohttp import ClientConnectionError +import pytest + +from homeassistant.components.daikin import update_unique_id +from homeassistant.components.daikin.const import DOMAIN, KEY_MAC +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .test_config_flow import HOST, MAC + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_daikin(): + """Mock pydaikin.""" + + async def mock_daikin_factory(*args, **kwargs): + """Mock the init function in pydaikin.""" + return Appliance + + with patch("homeassistant.components.daikin.Appliance") as Appliance: + Appliance.factory.side_effect = mock_daikin_factory + type(Appliance).update_status = AsyncMock() + type(Appliance).inside_temperature = PropertyMock(return_value=22) + type(Appliance).target_temperature = PropertyMock(return_value=22) + type(Appliance).zones = PropertyMock(return_value=[("Zone 1", "0", 0)]) + type(Appliance).fan_rate = PropertyMock(return_value=[]) + type(Appliance).swing_modes = PropertyMock(return_value=[]) + yield Appliance + + +DATA = { + "ver": "1_1_8", + "name": "DaikinAP00000", + "mac": MAC, + "model": "NOTSUPPORT", +} + + +INVALID_DATA = {**DATA, "name": None, "mac": HOST} + + +async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=HOST, + title=None, + data={CONF_HOST: HOST, KEY_MAC: HOST}, + ) + config_entry.add_to_hass(hass) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + type(mock_daikin).mac = PropertyMock(return_value=HOST) + type(mock_daikin).values = PropertyMock(return_value=INVALID_DATA) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.unique_id == HOST + + assert device_registry.async_get_device({}, {(KEY_MAC, HOST)}).name is None + + entity = entity_registry.async_get("climate.daikin_127_0_0_1") + assert entity.unique_id == HOST + assert update_unique_id(entity, MAC) is not None + + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(HOST) + + type(mock_daikin).mac = PropertyMock(return_value=MAC) + type(mock_daikin).values = PropertyMock(return_value=DATA) + + assert config_entry.unique_id != MAC + + assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.unique_id == MAC + + assert ( + device_registry.async_get_device({}, {(KEY_MAC, MAC)}).name == "DaikinAP00000" + ) + + entity = entity_registry.async_get("climate.daikin_127_0_0_1") + assert entity.unique_id == MAC + assert update_unique_id(entity, MAC) is None + + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(MAC) + + +async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MAC, + data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + config_entry.add_to_hass(hass) + + mock_daikin.factory.side_effect = ClientConnectionError + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MAC, + data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + config_entry.add_to_hass(hass) + + mock_daikin.factory.side_effect = asyncio.TimeoutError + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY