diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index e71c417da43..057947d76e4 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -1,7 +1,10 @@ """The Met Office integration.""" +from __future__ import annotations import asyncio import logging +import re +from typing import Any import datapoint @@ -13,8 +16,9 @@ from homeassistant.const import ( CONF_NAME, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -45,6 +49,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_key = entry.data[CONF_API_KEY] site_name = entry.data[CONF_NAME] + coordinates = f"{latitude}_{longitude}" + + @callback + def update_unique_id( + entity_entry: entity_registry.RegistryEntry, + ) -> dict[str, Any] | None: + """Update unique ID of entity entry.""" + + if entity_entry.domain != Platform.SENSOR: + return None + + name_to_key = { + "Station Name": "name", + "Weather": "weather", + "Temperature": "temperature", + "Feels Like Temperature": "feels_like_temperature", + "Wind Speed": "wind_speed", + "Wind Direction": "wind_direction", + "Wind Gust": "wind_gust", + "Visibility": "visibility", + "Visibility Distance": "visibility_distance", + "UV Index": "uv", + "Probability of Precipitation": "precipitation", + "Humidity": "humidity", + } + + match = re.search(f"(?P.*)_{coordinates}.*", entity_entry.unique_id) + + if match is None: + return None + + if (name := match.group("name")) in name_to_key: + return { + "new_unique_id": entity_entry.unique_id.replace(name, name_to_key[name]) + } + return None + + await entity_registry.async_migrate_entries(hass, entry.entry_id, update_unique_id) + connection = datapoint.connection(api_key=api_key) site = await hass.async_add_executor_job( @@ -84,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator, METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator, METOFFICE_NAME: site_name, - METOFFICE_COORDINATES: f"{latitude}_{longitude}", + METOFFICE_COORDINATES: coordinates, } # Fetch initial data so we have data when entities subscribe diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index 12f88cc6d56..e4843d1235e 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -33,9 +33,7 @@ METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions" METOFFICE_NAME = "metoffice_name" MODE_3HOURLY = "3hourly" -MODE_3HOURLY_LABEL = "3-Hourly" MODE_DAILY = "daily" -MODE_DAILY_LABEL = "Daily" CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["0"], diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index e24e2299be4..dd8ceefad23 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -35,9 +35,7 @@ from .const import ( METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, - MODE_3HOURLY_LABEL, MODE_DAILY, - MODE_DAILY_LABEL, VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) @@ -52,7 +50,7 @@ ATTR_SITE_NAME = "site_name" SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="name", - name="Station Name", + name="Station name", device_class=None, native_unit_of_measurement=None, icon="mdi:label-outline", @@ -76,7 +74,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="feels_like_temperature", - name="Feels Like Temperature", + name="Feels like temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, icon=None, @@ -84,7 +82,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wind_speed", - name="Wind Speed", + name="Wind speed", device_class=None, native_unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", @@ -92,7 +90,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wind_direction", - name="Wind Direction", + name="Wind direction", device_class=None, native_unit_of_measurement=None, icon="mdi:compass-outline", @@ -100,7 +98,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wind_gust", - name="Wind Gust", + name="Wind gust", device_class=None, native_unit_of_measurement=SPEED_MILES_PER_HOUR, icon="mdi:weather-windy", @@ -116,7 +114,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="visibility_distance", - name="Visibility Distance", + name="Visibility distance", device_class=None, native_unit_of_measurement=LENGTH_KILOMETERS, icon="mdi:eye", @@ -124,7 +122,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="uv", - name="UV Index", + name="UV index", device_class=None, native_unit_of_measurement=UV_INDEX, icon="mdi:weather-sunny-alert", @@ -132,7 +130,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="precipitation", - name="Probability of Precipitation", + name="Probability of precipitation", device_class=None, native_unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", @@ -183,6 +181,8 @@ class MetOfficeCurrentSensor( ): """Implementation of a Met Office current weather condition sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[MetOfficeData], @@ -194,13 +194,13 @@ class MetOfficeCurrentSensor( super().__init__(coordinator) self.entity_description = description - mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + mode_label = "3-hourly" if use_3hourly else "daily" self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) - self._attr_name = f"{hass_data[METOFFICE_NAME]} {description.name} {mode_label}" - self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" + self._attr_name = f"{description.name} {mode_label}" + self._attr_unique_id = f"{description.key}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" self._attr_entity_registry_enabled_default = ( diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 184782d4c12..4733ca6ea73 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -27,15 +27,12 @@ from . import get_device_info from .const import ( ATTRIBUTION, CONDITION_CLASSES, - DEFAULT_NAME, DOMAIN, METOFFICE_COORDINATES, METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, - MODE_3HOURLY_LABEL, MODE_DAILY, - MODE_DAILY_LABEL, ) from .data import MetOfficeData @@ -83,6 +80,7 @@ class MetOfficeWeather( """Implementation of a Met Office weather condition.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_pressure_unit = PRESSURE_HPA @@ -97,11 +95,10 @@ class MetOfficeWeather( """Initialise the platform with a data instance.""" super().__init__(coordinator) - mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) - self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" + self._attr_name = "3-Hourly" if use_3hourly else "Daily" self._attr_unique_id = hass_data[METOFFICE_COORDINATES] if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" diff --git a/tests/components/metoffice/const.py b/tests/components/metoffice/const.py index 56383764d08..8fe1b42ca59 100644 --- a/tests/components/metoffice/const.py +++ b/tests/components/metoffice/const.py @@ -11,10 +11,14 @@ TEST_LATITUDE_WAVERTREE = 53.38374 TEST_LONGITUDE_WAVERTREE = -2.90929 TEST_SITE_NAME_WAVERTREE = "Wavertree" +TEST_COORDINATES_WAVERTREE = f"{TEST_LATITUDE_WAVERTREE}_{TEST_LONGITUDE_WAVERTREE}" + TEST_LATITUDE_KINGSLYNN = 52.75556 TEST_LONGITUDE_KINGSLYNN = 0.44231 TEST_SITE_NAME_KINGSLYNN = "King's Lynn" +TEST_COORDINATES_KINGSLYNN = f"{TEST_LATITUDE_KINGSLYNN}_{TEST_LONGITUDE_KINGSLYNN}" + METOFFICE_CONFIG_WAVERTREE = { CONF_API_KEY: TEST_API_KEY, CONF_LATITUDE: TEST_LATITUDE_WAVERTREE, @@ -57,9 +61,5 @@ WAVERTREE_SENSOR_RESULTS = { "humidity": ("humidity", "50"), } -DEVICE_KEY_KINGSLYNN = { - (DOMAIN, f"{TEST_LATITUDE_KINGSLYNN}_{TEST_LONGITUDE_KINGSLYNN}") -} -DEVICE_KEY_WAVERTREE = { - (DOMAIN, f"{TEST_LATITUDE_WAVERTREE}_{TEST_LONGITUDE_WAVERTREE}") -} +DEVICE_KEY_KINGSLYNN = {(DOMAIN, TEST_COORDINATES_KINGSLYNN)} +DEVICE_KEY_WAVERTREE = {(DOMAIN, TEST_COORDINATES_WAVERTREE)} diff --git a/tests/components/metoffice/test_init.py b/tests/components/metoffice/test_init.py new file mode 100644 index 00000000000..b896c5319e2 --- /dev/null +++ b/tests/components/metoffice/test_init.py @@ -0,0 +1,125 @@ +"""Tests for metoffice init.""" +from __future__ import annotations + +import datetime + +from freezegun import freeze_time +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN, METOFFICE_CONFIG_WAVERTREE, TEST_COORDINATES_WAVERTREE + +from tests.common import MockConfigEntry + + +@freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc)) +@pytest.mark.parametrize( + "old_unique_id,new_unique_id,migration_needed", + [ + ( + f"Station Name_{TEST_COORDINATES_WAVERTREE}", + f"name_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Weather_{TEST_COORDINATES_WAVERTREE}", + f"weather_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Temperature_{TEST_COORDINATES_WAVERTREE}", + f"temperature_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Feels Like Temperature_{TEST_COORDINATES_WAVERTREE}", + f"feels_like_temperature_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Wind Speed_{TEST_COORDINATES_WAVERTREE}", + f"wind_speed_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Wind Direction_{TEST_COORDINATES_WAVERTREE}", + f"wind_direction_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Wind Gust_{TEST_COORDINATES_WAVERTREE}", + f"wind_gust_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Visibility_{TEST_COORDINATES_WAVERTREE}", + f"visibility_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Visibility Distance_{TEST_COORDINATES_WAVERTREE}", + f"visibility_distance_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"UV Index_{TEST_COORDINATES_WAVERTREE}", + f"uv_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Probability of Precipitation_{TEST_COORDINATES_WAVERTREE}", + f"precipitation_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"Humidity_{TEST_COORDINATES_WAVERTREE}", + f"humidity_{TEST_COORDINATES_WAVERTREE}", + True, + ), + ( + f"name_{TEST_COORDINATES_WAVERTREE}", + f"name_{TEST_COORDINATES_WAVERTREE}", + False, + ), + ("abcde", "abcde", False), + ], +) +async def test_migrate_unique_id( + hass, + old_unique_id: str, + new_unique_id: str, + migration_needed: bool, + requests_mock, +): + """Test unique id migration.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) + entry.add_to_hass(hass) + + ent_reg = er.async_get(hass) + + entity: er.RegistryEntry = ent_reg.async_get_or_create( + suggested_object_id="my_sensor", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=entry, + ) + assert entity.unique_id == old_unique_id + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + if migration_needed: + assert ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) is None + + assert ( + ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, new_unique_id) + == "sensor.my_sensor" + )