diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 71ca2af9537..f540c6d0b9c 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -7,22 +7,27 @@ from typing import Any from plugwise.exceptions import InvalidAuthentication, PlugwiseException from plugwise.smile import Smile -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER, PLATFORMS_GATEWAY +from .const import ( + DEFAULT_PORT, + DEFAULT_USERNAME, + DOMAIN, + LOGGER, + PLATFORMS_GATEWAY, + Platform, +) from .coordinator import PlugwiseDataUpdateCoordinator async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise Smiles from a config entry.""" - await async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) + await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) websession = async_get_clientsession(hass, verify_ssl=False) api = Smile( @@ -57,6 +62,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = PlugwiseDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() + migrate_sensor_entities(hass, coordinator) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator @@ -85,13 +91,39 @@ async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry): @callback -def async_migrate_entity_entry(entry: RegistryEntry) -> dict[str, Any] | None: +def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: """Migrate Plugwise entity entries. - Migrates unique ID from old relay switches to the new unique ID """ - if entry.domain == SWITCH_DOMAIN and entry.unique_id.endswith("-plug"): + if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"): return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")} # No migration needed return None + + +def migrate_sensor_entities( + hass: HomeAssistant, + coordinator: PlugwiseDataUpdateCoordinator, +) -> None: + """Migrate Sensors if needed.""" + ent_reg = er.async_get(hass) + + # Migrating opentherm_outdoor_temperature to opentherm_outdoor_air_temperature sensor + for device_id, device in coordinator.data.devices.items(): + if device["class"] != "heater_central": + continue + + old_unique_id = f"{device_id}-outdoor_temperature" + if entity_id := ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, old_unique_id + ): + new_unique_id = f"{device_id}-outdoor_air_temperature" + LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index f0624f8c026..bcbd6cd0b48 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.16.8"], + "requirements": ["plugwise==0.16.9"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 9099ee96990..87c81699d10 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -60,6 +60,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="outdoor_air_temperature", + name="Outdoor Air Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key="water_temperature", name="Water Temperature", diff --git a/requirements_all.txt b/requirements_all.txt index 14d62f29904..ee5fb6db55b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1222,7 +1222,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.16.8 +plugwise==0.16.9 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 885d4dc5973..eaff1d388ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -816,7 +816,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.16.8 +plugwise==0.16.9 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json index 4f9eda3739b..f70dbe275b7 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -25,12 +25,12 @@ "flame_state": false }, "sensors": { - "outdoor_temperature": 3.0, "water_temperature": 29.1, "intended_boiler_temperature": 0.0, "modulation_level": 52, "return_temperature": 25.1, - "water_pressure": 1.57 + "water_pressure": 1.57, + "outdoor_air_temperature": 3.0 }, "switches": { "dhw_cm_switch": false diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index d63b81bd0d3..811018db7e2 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -10,11 +10,17 @@ from plugwise.exceptions import ( import pytest from homeassistant.components.plugwise.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry +HEATER_ID = "1cbf783bb11e4a7c8a6843dee3a86927" # Opentherm device_id for migration +PLUG_ID = "cd0ddb54ef694e11ac18ed1cbce5dbbd" # VCR device_id for migration + async def test_load_unload_config_entry( hass: HomeAssistant, @@ -60,3 +66,85 @@ async def test_config_entry_not_ready( assert len(mock_smile_anna.connect.mock_calls) == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + "entitydata,old_unique_id,new_unique_id", + [ + ( + { + "domain": SENSOR_DOMAIN, + "platform": DOMAIN, + "unique_id": f"{HEATER_ID}-outdoor_temperature", + "suggested_object_id": f"{HEATER_ID}-outdoor_temperature", + "disabled_by": None, + }, + f"{HEATER_ID}-outdoor_temperature", + f"{HEATER_ID}-outdoor_air_temperature", + ), + ], +) +async def test_migrate_unique_id_temperature( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smile_anna: MagicMock, + entitydata: dict, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + mock_config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + **entitydata, + config_entry=mock_config_entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id + + +@pytest.mark.parametrize( + "entitydata,old_unique_id,new_unique_id", + [ + ( + { + "domain": SWITCH_DOMAIN, + "platform": DOMAIN, + "unique_id": f"{PLUG_ID}-plug", + "suggested_object_id": f"{PLUG_ID}-plug", + "disabled_by": None, + }, + f"{PLUG_ID}-plug", + f"{PLUG_ID}-relay", + ), + ], +) +async def test_migrate_unique_id_relay( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smile_adam: MagicMock, + entitydata: dict, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + mock_config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + **entitydata, + config_entry=mock_config_entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index d5edeae3508..2b4baccc7c9 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -39,7 +39,7 @@ async def test_anna_as_smt_climate_sensor_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: """Test creation of climate related sensor entities.""" - state = hass.states.get("sensor.opentherm_outdoor_temperature") + state = hass.states.get("sensor.opentherm_outdoor_air_temperature") assert state assert float(state.state) == 3.0 @@ -56,7 +56,7 @@ async def test_anna_climate_sensor_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: """Test creation of climate related sensor entities.""" - state = hass.states.get("sensor.opentherm_outdoor_temperature") + state = hass.states.get("sensor.opentherm_outdoor_air_temperature") assert state assert float(state.state) == 3.0