From 0d9ac252577e15f2a9311dda5bc9861af26b4dcf Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:28:10 +0000 Subject: [PATCH] Add and cleanup tplink translations (#135120) --- homeassistant/components/tplink/__init__.py | 33 +++++++++--- .../components/tplink/binary_sensor.py | 5 -- homeassistant/components/tplink/climate.py | 10 +++- homeassistant/components/tplink/entity.py | 24 ++++++++- homeassistant/components/tplink/icons.json | 6 --- homeassistant/components/tplink/sensor.py | 5 -- homeassistant/components/tplink/strings.json | 52 ++++--------------- .../tplink/snapshots/test_binary_sensor.ambr | 6 +-- .../tplink/snapshots/test_sensor.ambr | 12 ++--- tests/components/tplink/test_switch.py | 20 ++++++- 10 files changed, 97 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 13261ed752e..90f97e113ca 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Iterable from datetime import timedelta import logging -from typing import Any +from typing import Any, cast from aiohttp import ClientSession from kasa import ( @@ -178,9 +178,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo if not credentials and entry_credentials_hash: data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH} hass.config_entries.async_update_entry(entry, data=data) - raise ConfigEntryAuthFailed from ex + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="device_authentication", + translation_placeholders={ + "func": "connect", + "exc": str(ex), + }, + ) from ex except KasaException as ex: - raise ConfigEntryNotReady from ex + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="device_error", + translation_placeholders={ + "func": "connect", + "exc": str(ex), + }, + ) from ex device_credentials_hash = device.credentials_hash @@ -212,7 +226,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo # wait for the next discovery to find the device at its new address # and update the config entry so we do not mix up devices. raise ConfigEntryNotReady( - f"Unexpected device found at {host}; expected {entry.unique_id}, found {found_mac}" + translation_domain=DOMAIN, + translation_key="unexpected_device", + translation_placeholders={ + "host": host, + # all entries have a unique id + "expected": cast(str, entry.unique_id), + "found": found_mac, + }, ) parent_coordinator = TPLinkDataUpdateCoordinator(hass, device, timedelta(seconds=5)) @@ -263,7 +284,7 @@ def legacy_device_id(device: Device) -> str: return device_id.split("_")[1] -def get_device_name(device: Device, parent: Device | None = None) -> str: +def get_device_name(device: Device, parent: Device | None = None) -> str | None: """Get a name for the device. alias can be none on some devices.""" if device.alias: return device.alias @@ -278,7 +299,7 @@ def get_device_name(device: Device, parent: Device | None = None) -> str: ] suffix = f" {devices.index(device.device_id) + 1}" if len(devices) > 1 else "" return f"{device.device_type.value.capitalize()}{suffix}" - return f"Unnamed {device.model}" + return None async def get_credentials(hass: HomeAssistant) -> Credentials | None: diff --git a/homeassistant/components/tplink/binary_sensor.py b/homeassistant/components/tplink/binary_sensor.py index 318d0803e53..34f32ca3954 100644 --- a/homeassistant/components/tplink/binary_sensor.py +++ b/homeassistant/components/tplink/binary_sensor.py @@ -42,11 +42,6 @@ BINARY_SENSOR_DESCRIPTIONS: Final = ( key="cloud_connection", device_class=BinarySensorDeviceClass.CONNECTIVITY, ), - # To be replaced & disabled per default by the upcoming update platform. - TPLinkBinarySensorEntityDescription( - key="update_available", - device_class=BinarySensorDeviceClass.UPDATE, - ), TPLinkBinarySensorEntityDescription( key="temperature_warning", ), diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py index cef9a732cfd..e8b7336f391 100644 --- a/homeassistant/components/tplink/climate.py +++ b/homeassistant/components/tplink/climate.py @@ -21,7 +21,7 @@ from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TPLinkConfigEntry -from .const import UNIT_MAPPING +from .const import DOMAIN, UNIT_MAPPING from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity, async_refresh_after @@ -104,7 +104,13 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity): elif hvac_mode is HVACMode.OFF: await self._state_feature.set_value(False) else: - raise ServiceValidationError(f"Tried to set unsupported mode: {hvac_mode}") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="unsupported_mode", + translation_placeholders={ + "mode": hvac_mode, + }, + ) @async_refresh_after async def async_turn_on(self) -> None: diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 935857e5db1..01342339bef 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -162,6 +162,9 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB registry_device = device device_name = get_device_name(device, parent=parent) + translation_key: str | None = None + translation_placeholders: Mapping[str, str] | None = None + if parent and parent.device_type is not Device.Type.Hub: if not feature or feature.id == PRIMARY_STATE_ID: # Entity will be added to parent if not a hub and no feature @@ -169,6 +172,9 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB # is the primary state registry_device = parent device_name = get_device_name(registry_device) + if not device_name: + translation_key = "unnamed_device" + translation_placeholders = {"model": parent.model} else: # Prefix the device name with the parent name unless it is a # hub attached device. Sensible default for child devices like @@ -177,13 +183,28 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB # Bedroom Ceiling Fan; Child device aliases will be Ceiling Fan # and Dimmer Switch for both so should be distinguished by the # parent name. - device_name = f"{get_device_name(parent)} {get_device_name(device, parent=parent)}" + parent_device_name = get_device_name(parent) + child_device_name = get_device_name(device, parent=parent) + if parent_device_name: + device_name = f"{parent_device_name} {child_device_name}" + else: + device_name = None + translation_key = "unnamed_device" + translation_placeholders = { + "model": f"{parent.model} {child_device_name}" + } + + if device_name is None and not translation_key: + translation_key = "unnamed_device" + translation_placeholders = {"model": device.model} self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, str(registry_device.device_id))}, manufacturer="TP-Link", model=registry_device.model, name=device_name, + translation_key=translation_key, + translation_placeholders=translation_placeholders, sw_version=registry_device.hw_info["sw_ver"], hw_version=registry_device.hw_info["hw_ver"], ) @@ -320,6 +341,7 @@ class CoordinatedTPLinkFeatureEntity(CoordinatedTPLinkEntity, ABC): if descriptions and (desc := descriptions.get(feature.id)): translation_key: str | None = feature.id + # HA logic is to name entities based on the following logic: # _attr_name > translation.name > description.name # > device_class (if base platform supports). diff --git a/homeassistant/components/tplink/icons.json b/homeassistant/components/tplink/icons.json index 9cc0326b59f..aedbccfbd51 100644 --- a/homeassistant/components/tplink/icons.json +++ b/homeassistant/components/tplink/icons.json @@ -125,12 +125,6 @@ "signal_level": { "default": "mdi:signal" }, - "current_firmware_version": { - "default": "mdi:information" - }, - "available_firmware_version": { - "default": "mdi:information-outline" - }, "alarm_source": { "default": "mdi:bell" }, diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index e18a849ccd6..59e29d7a010 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -116,11 +116,6 @@ SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( key="alarm_source", ), - TPLinkSensorEntityDescription( - key="temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), ) SENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in SENSOR_DESCRIPTIONS} diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index 9cf302ed717..9c32dd5bbf4 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -109,26 +109,9 @@ "overheated": { "name": "Overheated" }, - "battery_low": { - "name": "Battery low" - }, "cloud_connection": { "name": "Cloud connection" }, - "update_available": { - "name": "[%key:component::binary_sensor::entity_component::update::name%]", - "state": { - "off": "[%key:component::binary_sensor::entity_component::update::state::off%]", - "on": "[%key:component::binary_sensor::entity_component::update::state::on%]" - } - }, - "is_open": { - "name": "[%key:component::binary_sensor::entity_component::door::name%]", - "state": { - "off": "[%key:common::state::closed%]", - "on": "[%key:common::state::open%]" - } - }, "water_alert": { "name": "[%key:component::binary_sensor::entity_component::moisture::name%]", "state": { @@ -195,27 +178,6 @@ "signal_level": { "name": "Signal level" }, - "current_firmware_version": { - "name": "Current firmware version" - }, - "available_firmware_version": { - "name": "Available firmware version" - }, - "battery_level": { - "name": "Battery level" - }, - "temperature": { - "name": "[%key:component::sensor::entity_component::temperature::name%]" - }, - "voltage": { - "name": "[%key:component::sensor::entity_component::voltage::name%]" - }, - "current": { - "name": "[%key:component::sensor::entity_component::current::name%]" - }, - "humidity": { - "name": "[%key:component::sensor::entity_component::humidity::name%]" - }, "device_time": { "name": "Device time" }, @@ -230,9 +192,6 @@ }, "alarm_source": { "name": "Alarm source" - }, - "rssi": { - "name": "[%key:component::sensor::entity_component::signal_strength::name%]" } }, "switch": { @@ -291,6 +250,11 @@ } } }, + "device": { + "unnamed_device": { + "name": "Unnamed {model}" + } + }, "services": { "sequence_effect": { "name": "Sequence effect", @@ -397,6 +361,12 @@ }, "set_custom_effect": { "message": "Error trying to set custom effect {effect}: {exc}" + }, + "unexpected_device": { + "message": "Unexpected device found at {host}; expected {expected}, found {found}" + }, + "unsupported_mode": { + "message": "Tried to set unsupported mode: {mode}" } }, "issues": { diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index 4a1cfe5b411..e16d4409511 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_states[binary_sensor.my_device_battery_low-entry] +# name: test_states[binary_sensor.my_device_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -11,7 +11,7 @@ 'disabled_by': , 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.my_device_battery_low', + 'entity_id': 'binary_sensor.my_device_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -23,7 +23,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery low', + 'original_name': 'Battery', 'platform': 'tplink', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 739f02e51f0..461e8c6e505 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -115,7 +115,7 @@ 'state': '2024-06-24T09:03:11+00:00', }) # --- -# name: test_states[sensor.my_device_battery_level-entry] +# name: test_states[sensor.my_device_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -129,7 +129,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.my_device_battery_level', + 'entity_id': 'sensor.my_device_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -141,7 +141,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'tplink', 'previous_unique_id': None, 'supported_features': 0, @@ -150,16 +150,16 @@ 'unit_of_measurement': '%', }) # --- -# name: test_states[sensor.my_device_battery_level-state] +# name: test_states[sensor.my_device_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'my_device Battery level', + 'friendly_name': 'my_device Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.my_device_battery_level', + 'entity_id': 'sensor.my_device_battery', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index e9c8cc07b67..47b2e078f5a 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -260,7 +260,9 @@ async def test_strip_unique_ids( async def test_strip_blank_alias( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, ) -> None: """Test a strip unique id.""" already_migrated_config_entry = MockConfigEntry( @@ -277,11 +279,27 @@ async def test_strip_blank_alias( await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() + strip_entity_id = "switch.unnamed_ks123" + state = hass.states.get(strip_entity_id) + assert state.name == "Unnamed KS123" + reg_ent = entity_registry.async_get(strip_entity_id) + assert reg_ent + reg_dev = device_registry.async_get(reg_ent.device_id) + assert reg_dev + assert reg_dev.name == "Unnamed KS123" + for plug_id in range(2): entity_id = f"switch.unnamed_ks123_stripsocket_{plug_id + 1}" state = hass.states.get(entity_id) assert state.name == f"Unnamed KS123 Stripsocket {plug_id + 1}" + reg_ent = entity_registry.async_get(entity_id) + assert reg_ent + reg_dev = device_registry.async_get(reg_ent.device_id) + assert reg_dev + # Switch is a primary feature so entities go on the parent device. + assert reg_dev.name == "Unnamed KS123" + @pytest.mark.parametrize( ("exception_type", "msg", "reauth_expected"),