Add and cleanup tplink translations (#135120)

This commit is contained in:
Steven B. 2025-01-09 10:28:10 +00:00 committed by GitHub
parent 15e785b974
commit 0d9ac25257
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 97 additions and 76 deletions

View File

@ -6,7 +6,7 @@ import asyncio
from collections.abc import Iterable from collections.abc import Iterable
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any, cast
from aiohttp import ClientSession from aiohttp import ClientSession
from kasa import ( from kasa import (
@ -178,9 +178,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
if not credentials and entry_credentials_hash: if not credentials and entry_credentials_hash:
data = {k: v for k, v in entry.data.items() if k != CONF_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) 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: 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 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 # 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. # and update the config entry so we do not mix up devices.
raise ConfigEntryNotReady( 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)) parent_coordinator = TPLinkDataUpdateCoordinator(hass, device, timedelta(seconds=5))
@ -263,7 +284,7 @@ def legacy_device_id(device: Device) -> str:
return device_id.split("_")[1] 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.""" """Get a name for the device. alias can be none on some devices."""
if device.alias: if device.alias:
return 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 "" suffix = f" {devices.index(device.device_id) + 1}" if len(devices) > 1 else ""
return f"{device.device_type.value.capitalize()}{suffix}" return f"{device.device_type.value.capitalize()}{suffix}"
return f"Unnamed {device.model}" return None
async def get_credentials(hass: HomeAssistant) -> Credentials | None: async def get_credentials(hass: HomeAssistant) -> Credentials | None:

View File

@ -42,11 +42,6 @@ BINARY_SENSOR_DESCRIPTIONS: Final = (
key="cloud_connection", key="cloud_connection",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
), ),
# To be replaced & disabled per default by the upcoming update platform.
TPLinkBinarySensorEntityDescription(
key="update_available",
device_class=BinarySensorDeviceClass.UPDATE,
),
TPLinkBinarySensorEntityDescription( TPLinkBinarySensorEntityDescription(
key="temperature_warning", key="temperature_warning",
), ),

View File

@ -21,7 +21,7 @@ from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import TPLinkConfigEntry from . import TPLinkConfigEntry
from .const import UNIT_MAPPING from .const import DOMAIN, UNIT_MAPPING
from .coordinator import TPLinkDataUpdateCoordinator from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, async_refresh_after from .entity import CoordinatedTPLinkEntity, async_refresh_after
@ -104,7 +104,13 @@ class TPLinkClimateEntity(CoordinatedTPLinkEntity, ClimateEntity):
elif hvac_mode is HVACMode.OFF: elif hvac_mode is HVACMode.OFF:
await self._state_feature.set_value(False) await self._state_feature.set_value(False)
else: 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_refresh_after
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:

View File

@ -162,6 +162,9 @@ class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator], AB
registry_device = device registry_device = device
device_name = get_device_name(device, parent=parent) 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 parent and parent.device_type is not Device.Type.Hub:
if not feature or feature.id == PRIMARY_STATE_ID: if not feature or feature.id == PRIMARY_STATE_ID:
# Entity will be added to parent if not a hub and no feature # 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 # is the primary state
registry_device = parent registry_device = parent
device_name = get_device_name(registry_device) device_name = get_device_name(registry_device)
if not device_name:
translation_key = "unnamed_device"
translation_placeholders = {"model": parent.model}
else: else:
# Prefix the device name with the parent name unless it is a # Prefix the device name with the parent name unless it is a
# hub attached device. Sensible default for child devices like # 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 # Bedroom Ceiling Fan; Child device aliases will be Ceiling Fan
# and Dimmer Switch for both so should be distinguished by the # and Dimmer Switch for both so should be distinguished by the
# parent name. # 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( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(registry_device.device_id))}, identifiers={(DOMAIN, str(registry_device.device_id))},
manufacturer="TP-Link", manufacturer="TP-Link",
model=registry_device.model, model=registry_device.model,
name=device_name, name=device_name,
translation_key=translation_key,
translation_placeholders=translation_placeholders,
sw_version=registry_device.hw_info["sw_ver"], sw_version=registry_device.hw_info["sw_ver"],
hw_version=registry_device.hw_info["hw_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)): if descriptions and (desc := descriptions.get(feature.id)):
translation_key: str | None = feature.id translation_key: str | None = feature.id
# HA logic is to name entities based on the following logic: # HA logic is to name entities based on the following logic:
# _attr_name > translation.name > description.name # _attr_name > translation.name > description.name
# > device_class (if base platform supports). # > device_class (if base platform supports).

View File

@ -125,12 +125,6 @@
"signal_level": { "signal_level": {
"default": "mdi:signal" "default": "mdi:signal"
}, },
"current_firmware_version": {
"default": "mdi:information"
},
"available_firmware_version": {
"default": "mdi:information-outline"
},
"alarm_source": { "alarm_source": {
"default": "mdi:bell" "default": "mdi:bell"
}, },

View File

@ -116,11 +116,6 @@ SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = (
TPLinkSensorEntityDescription( TPLinkSensorEntityDescription(
key="alarm_source", 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} SENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in SENSOR_DESCRIPTIONS}

View File

@ -109,26 +109,9 @@
"overheated": { "overheated": {
"name": "Overheated" "name": "Overheated"
}, },
"battery_low": {
"name": "Battery low"
},
"cloud_connection": { "cloud_connection": {
"name": "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": { "water_alert": {
"name": "[%key:component::binary_sensor::entity_component::moisture::name%]", "name": "[%key:component::binary_sensor::entity_component::moisture::name%]",
"state": { "state": {
@ -195,27 +178,6 @@
"signal_level": { "signal_level": {
"name": "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": { "device_time": {
"name": "Device time" "name": "Device time"
}, },
@ -230,9 +192,6 @@
}, },
"alarm_source": { "alarm_source": {
"name": "Alarm source" "name": "Alarm source"
},
"rssi": {
"name": "[%key:component::sensor::entity_component::signal_strength::name%]"
} }
}, },
"switch": { "switch": {
@ -291,6 +250,11 @@
} }
} }
}, },
"device": {
"unnamed_device": {
"name": "Unnamed {model}"
}
},
"services": { "services": {
"sequence_effect": { "sequence_effect": {
"name": "Sequence effect", "name": "Sequence effect",
@ -397,6 +361,12 @@
}, },
"set_custom_effect": { "set_custom_effect": {
"message": "Error trying to set custom effect {effect}: {exc}" "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": { "issues": {

View File

@ -1,5 +1,5 @@
# serializer version: 1 # serializer version: 1
# name: test_states[binary_sensor.my_device_battery_low-entry] # name: test_states[binary_sensor.my_device_battery-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -11,7 +11,7 @@
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>, 'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'binary_sensor', 'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.my_device_battery_low', 'entity_id': 'binary_sensor.my_device_battery',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
@ -23,7 +23,7 @@
}), }),
'original_device_class': <BinarySensorDeviceClass.BATTERY: 'battery'>, 'original_device_class': <BinarySensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Battery low', 'original_name': 'Battery',
'platform': 'tplink', 'platform': 'tplink',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,

View File

@ -115,7 +115,7 @@
'state': '2024-06-24T09:03:11+00:00', '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({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -129,7 +129,7 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'sensor', 'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>, 'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_battery_level', 'entity_id': 'sensor.my_device_battery',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
@ -141,7 +141,7 @@
}), }),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Battery level', 'original_name': 'Battery',
'platform': 'tplink', 'platform': 'tplink',
'previous_unique_id': None, 'previous_unique_id': None,
'supported_features': 0, 'supported_features': 0,
@ -150,16 +150,16 @@
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}) })
# --- # ---
# name: test_states[sensor.my_device_battery_level-state] # name: test_states[sensor.my_device_battery-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'battery', 'device_class': 'battery',
'friendly_name': 'my_device Battery level', 'friendly_name': 'my_device Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.my_device_battery_level', 'entity_id': 'sensor.my_device_battery',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,

View File

@ -260,7 +260,9 @@ async def test_strip_unique_ids(
async def test_strip_blank_alias( async def test_strip_blank_alias(
hass: HomeAssistant, entity_registry: er.EntityRegistry hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None: ) -> None:
"""Test a strip unique id.""" """Test a strip unique id."""
already_migrated_config_entry = MockConfigEntry( 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 async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done() 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): for plug_id in range(2):
entity_id = f"switch.unnamed_ks123_stripsocket_{plug_id + 1}" entity_id = f"switch.unnamed_ks123_stripsocket_{plug_id + 1}"
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.name == f"Unnamed KS123 Stripsocket {plug_id + 1}" 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( @pytest.mark.parametrize(
("exception_type", "msg", "reauth_expected"), ("exception_type", "msg", "reauth_expected"),