Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 84 additions & 2 deletions homeassistant/components/daikin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -127,3 +130,82 @@ def device_info(self) -> DeviceInfo:
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}
2 changes: 1 addition & 1 deletion homeassistant/components/daikin/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."]
}
2 changes: 1 addition & 1 deletion homeassistant/components/daikin/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1615,7 +1615,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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,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
Expand Down
128 changes: 128 additions & 0 deletions tests/components/daikin/test_init.py
Original file line number Diff line number Diff line change
@@ -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