diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index d8623e7bbe5..43a37179b03 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta from functools import partial import logging +import re from typing import Any import transmission_rpc @@ -18,15 +19,20 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_ID, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_USERNAME, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + selector, +) from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -91,9 +97,41 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.SENSOR, Platform.SWITCH] +MIGRATION_NAME_TO_KEY = { + # Sensors + "Down Speed": "download", + "Up Speed": "upload", + "Status": "status", + "Active Torrents": "active_torrents", + "Paused Torrents": "paused_torrents", + "Total Torrents": "total_torrents", + "Completed Torrents": "completed_torrents", + "Started Torrents": "started_torrents", + # Switches + "Switch": "on_off", + "Turtle Mode": "turtle_mode", +} + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Transmission Component.""" + + @callback + def update_unique_id( + entity_entry: er.RegistryEntry, + ) -> dict[str, Any] | None: + """Update unique ID of entity entry.""" + match = re.search( + f"{config_entry.data[CONF_HOST]}-{config_entry.data[CONF_NAME]} (?P.+)", + entity_entry.unique_id, + ) + + if match and (key := MIGRATION_NAME_TO_KEY.get(match.group("name"))): + return {"new_unique_id": f"{config_entry.entry_id}-{key}"} + return None + + await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + client = TransmissionClient(hass, config_entry) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 184d05faeb0..946ff42f59b 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -40,12 +40,20 @@ async def async_setup_entry( dev = [ TransmissionSpeedSensor(tm_client, name, "Down Speed", "download"), TransmissionSpeedSensor(tm_client, name, "Up Speed", "upload"), - TransmissionStatusSensor(tm_client, name, "Status"), - TransmissionTorrentsSensor(tm_client, name, "Active Torrents", "active"), - TransmissionTorrentsSensor(tm_client, name, "Paused Torrents", "paused"), - TransmissionTorrentsSensor(tm_client, name, "Total Torrents", "total"), - TransmissionTorrentsSensor(tm_client, name, "Completed Torrents", "completed"), - TransmissionTorrentsSensor(tm_client, name, "Started Torrents", "started"), + TransmissionStatusSensor(tm_client, name, "Status", "status"), + TransmissionTorrentsSensor( + tm_client, name, "Active Torrents", "active_torrents" + ), + TransmissionTorrentsSensor( + tm_client, name, "Paused Torrents", "paused_torrents" + ), + TransmissionTorrentsSensor(tm_client, name, "Total Torrents", "total_torrents"), + TransmissionTorrentsSensor( + tm_client, name, "Completed Torrents", "completed_torrents" + ), + TransmissionTorrentsSensor( + tm_client, name, "Started Torrents", "started_torrents" + ), ] async_add_entities(dev, True) @@ -56,13 +64,13 @@ class TransmissionSensor(SensorEntity): _attr_should_poll = False - def __init__(self, tm_client, client_name, sensor_name, sub_type=None): + def __init__(self, tm_client, client_name, sensor_name, key): """Initialize the sensor.""" self._tm_client: TransmissionClient = tm_client - self._client_name = client_name - self._name = sensor_name - self._sub_type = sub_type + self._attr_name = f"{client_name} {sensor_name}" + self._key = key self._state = None + self._attr_unique_id = f"{tm_client.config_entry.entry_id}-{key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, tm_client.config_entry.entry_id)}, @@ -70,16 +78,6 @@ class TransmissionSensor(SensorEntity): name=client_name, ) - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._client_name} {self._name}" - - @property - def unique_id(self): - """Return the unique id of the entity.""" - return f"{self._tm_client.api.host}-{self.name}" - @property def native_value(self): """Return the state of the sensor.""" @@ -118,7 +116,7 @@ class TransmissionSpeedSensor(TransmissionSensor): if data := self._tm_client.api.data: b_spd = ( float(data.download_speed) - if self._sub_type == "download" + if self._key == "download" else float(data.upload_speed) ) self._state = b_spd @@ -151,12 +149,15 @@ class TransmissionStatusSensor(TransmissionSensor): class TransmissionTorrentsSensor(TransmissionSensor): """Representation of a Transmission torrents sensor.""" - SUBTYPE_MODES = { - "started": ("downloading"), - "completed": ("seeding"), - "paused": ("stopped"), - "active": ("seeding", "downloading"), - "total": None, + MODES: dict[str, list[str] | None] = { + "started_torrents": ["downloading"], + "completed_torrents": ["seeding"], + "paused_torrents": ["stopped"], + "active_torrents": [ + "seeding", + "downloading", + ], + "total_torrents": None, } @property @@ -171,7 +172,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): torrents=self._tm_client.api.torrents, order=self._tm_client.config_entry.options[CONF_ORDER], limit=self._tm_client.config_entry.options[CONF_LIMIT], - statuses=self.SUBTYPE_MODES[self._sub_type], + statuses=self.MODES[self._key], ) return { STATE_ATTR_TORRENT_INFO: info, @@ -180,7 +181,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): def update(self) -> None: """Get the latest data from Transmission and updates the state.""" torrents = _filter_torrents( - self._tm_client.api.torrents, statuses=self.SUBTYPE_MODES[self._sub_type] + self._tm_client.api.torrents, statuses=self.MODES[self._key] ) self._state = len(torrents) diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 3af3fe57e3c..d60a992e791 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -40,13 +40,13 @@ class TransmissionSwitch(SwitchEntity): def __init__(self, switch_type, switch_name, tm_client, client_name): """Initialize the Transmission switch.""" - self._name = switch_name - self.client_name = client_name + self._attr_name = f"{client_name} {switch_name}" self.type = switch_type self._tm_client = tm_client self._state = STATE_OFF self._data = None self.unsub_update = None + self._attr_unique_id = f"{tm_client.config_entry.entry_id}-{switch_type}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, tm_client.config_entry.entry_id)}, @@ -54,16 +54,6 @@ class TransmissionSwitch(SwitchEntity): name=client_name, ) - @property - def name(self): - """Return the name of the switch.""" - return f"{self.client_name} {self._name}" - - @property - def unique_id(self): - """Return the unique id of the entity.""" - return f"{self._tm_client.api.host}-{self.name}" - @property def is_on(self): """Return true if device is on.""" diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 89ad0dd2410..84bbf6be6ef 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -9,9 +9,12 @@ from transmission_rpc.error import ( TransmissionError, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.transmission.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from . import MOCK_CONFIG_DATA @@ -91,3 +94,68 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data[DOMAIN] + + +@pytest.mark.parametrize( + ("domain", "old_unique_id", "new_unique_id"), + [ + (SENSOR_DOMAIN, "0.0.0.0-Transmission Down Speed", "1234-download"), + (SENSOR_DOMAIN, "0.0.0.0-Transmission Up Speed", "1234-upload"), + (SENSOR_DOMAIN, "0.0.0.0-Transmission Status", "1234-status"), + ( + SENSOR_DOMAIN, + "0.0.0.0-Transmission Active Torrents", + "1234-active_torrents", + ), + ( + SENSOR_DOMAIN, + "0.0.0.0-Transmission Paused Torrents", + "1234-paused_torrents", + ), + (SENSOR_DOMAIN, "0.0.0.0-Transmission Total Torrents", "1234-total_torrents"), + ( + SENSOR_DOMAIN, + "0.0.0.0-Transmission Completed Torrents", + "1234-completed_torrents", + ), + ( + SENSOR_DOMAIN, + "0.0.0.0-Transmission Started Torrents", + "1234-started_torrents", + ), + # no change on correct sensor unique id + (SENSOR_DOMAIN, "1234-started_torrents", "1234-started_torrents"), + (SWITCH_DOMAIN, "0.0.0.0-Transmission Switch", "1234-on_off"), + (SWITCH_DOMAIN, "0.0.0.0-Transmission Turtle Mode", "1234-turtle_mode"), + # no change on correct switch unique id + (SWITCH_DOMAIN, "1234-turtle_mode", "1234-turtle_mode"), + ], +) +async def test_migrate_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + domain: str, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test unique id migration.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA, entry_id="1234") + entry.add_to_hass(hass) + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id=f"my_{domain}", + disabled_by=None, + domain=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() + + migrated_entity = entity_registry.async_get(entity.entity_id) + + assert migrated_entity + assert migrated_entity.unique_id == new_unique_id