diff --git a/homeassistant/components/tplink/icons.json b/homeassistant/components/tplink/icons.json index e00e8f69467..15e9406b2c9 100644 --- a/homeassistant/components/tplink/icons.json +++ b/homeassistant/components/tplink/icons.json @@ -113,6 +113,9 @@ "state": { "on": "mdi:baby-face" } + }, + "carpet_boost": { + "default": "mdi:rug" } }, "sensor": { @@ -130,6 +133,9 @@ }, "water_alert_timestamp": { "default": "mdi:clock-alert-outline" + }, + "vacuum_error": { + "default": "mdi:alert-circle" } }, "number": { @@ -150,6 +156,9 @@ }, "tilt_step": { "default": "mdi:unfold-more-horizontal" + }, + "clean_count": { + "default": "mdi:counter" } } }, diff --git a/homeassistant/components/tplink/number.py b/homeassistant/components/tplink/number.py index 0af2b7403e8..b47c50d688f 100644 --- a/homeassistant/components/tplink/number.py +++ b/homeassistant/components/tplink/number.py @@ -65,6 +65,10 @@ NUMBER_DESCRIPTIONS: Final = ( key="tilt_step", mode=NumberMode.BOX, ), + TPLinkNumberEntityDescription( + key="clean_count", + mode=NumberMode.SLIDER, + ), ) NUMBER_DESCRIPTIONS_MAP = {desc.key: desc for desc in NUMBER_DESCRIPTIONS} diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index aaba6b2674d..0f5dbc0a2e3 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -2,10 +2,12 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast from kasa import Feature +from kasa.smart.modules.clean import ErrorCode as VacuumError from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, @@ -28,6 +30,9 @@ class TPLinkSensorEntityDescription( ): """Base class for a TPLink feature based sensor entity description.""" + #: Optional callable to convert the value + convert_fn: Callable[[Any], Any] | None = None + # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -115,6 +120,12 @@ SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( key="alarm_source", ), + TPLinkSensorEntityDescription( + key="vacuum_error", + device_class=SensorDeviceClass.ENUM, + options=[name.lower() for name in VacuumError._member_names_], + convert_fn=lambda x: x.name.lower(), + ), ) SENSOR_DESCRIPTIONS_MAP = {desc.key: desc for desc in SENSOR_DESCRIPTIONS} @@ -165,6 +176,9 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): # We probably do not need this, when we are rounding already? self._attr_suggested_display_precision = self._feature.precision_hint + if self.entity_description.convert_fn: + value = self.entity_description.convert_fn(value) + if TYPE_CHECKING: # pylint: disable-next=import-outside-toplevel from datetime import date, datetime diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index 304bf353b7c..034aff7a763 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -198,6 +198,23 @@ }, "alarm_source": { "name": "Alarm source" + }, + "vacuum_error": { + "name": "Error", + "state": { + "ok": "No error", + "sidebrushstuck": "Side brush stuck", + "mainbrushstuck": "Main brush stuck", + "wheelblocked": "Wheel blocked", + "trapped": "Unable to move", + "trappedcliff": "Unable to move (cliff sensor)", + "dustbinremoved": "Missing dust bin", + "unabletomove": "Unable to move", + "lidarblocked": "Lidar blocked", + "unabletofinddock": "Unable to find dock", + "batterylow": "Low on battery", + "unknowninternal": "Unknown error, report to upstream" + } } }, "switch": { @@ -233,6 +250,9 @@ }, "baby_cry_detection": { "name": "Baby cry detection" + }, + "carpet_boost": { + "name": "Carpet boost" } }, "number": { @@ -253,6 +273,9 @@ }, "tilt_step": { "name": "Tilt degrees" + }, + "clean_count": { + "name": "Clean count" } } }, diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 04ca95273af..f08753def26 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -74,6 +74,9 @@ SWITCH_DESCRIPTIONS: tuple[TPLinkSwitchEntityDescription, ...] = ( TPLinkSwitchEntityDescription( key="baby_cry_detection", ), + TPLinkSwitchEntityDescription( + key="carpet_boost", + ), ) SWITCH_DESCRIPTIONS_MAP = {desc.key: desc for desc in SWITCH_DESCRIPTIONS} diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 664fb96fe71..028215dc157 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -60,6 +60,7 @@ def _load_feature_fixtures(): FEATURES_FIXTURE = _load_feature_fixtures() +FIXTURE_ENUM_TYPES = {"CleanErrorCode": ErrorCode} async def setup_platform_for_device( @@ -275,6 +276,10 @@ def _mocked_feature( if fixture := FEATURES_FIXTURE.get(id): # copy the fixture so tests do not interfere with each other fixture = dict(fixture) + if enum_type := fixture.get("enum_type"): + val = FIXTURE_ENUM_TYPES[enum_type](fixture["value"]) + fixture["value"] = val + else: assert require_fixture is False, ( f"No fixture defined for feature {id} and require_fixture is True" diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json index adb6c08ee50..d366a91c33c 100644 --- a/tests/components/tplink/fixtures/features.json +++ b/tests/components/tplink/fixtures/features.json @@ -371,6 +371,22 @@ "type": "Number", "category": "Config" }, + "clean_count": { + "value": 1, + "type": "Number", + "category": "Config" + }, + "carpet_boost": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "vacuum_error": { + "value": 0, + "type": "Sensor", + "category": "Info", + "enum_type": "CleanErrorCode" + }, "pair": { "value": "", "type": "Action", diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index df5ef71bf44..6733c5423a0 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -35,6 +35,61 @@ 'via_device_id': None, }) # --- +# name: test_states[number.my_device_clean_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_clean_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Clean count', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clean_count', + 'unique_id': '123456789ABCDEFGH_clean_count', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_clean_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Clean count', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_clean_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- # name: test_states[number.my_device_pan_degrees-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 461e8c6e505..e223a72dbc0 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -307,6 +307,82 @@ 'unit_of_measurement': None, }) # --- +# name: test_states[sensor.my_device_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'sidebrushstuck', + 'mainbrushstuck', + 'wheelblocked', + 'trapped', + 'trappedcliff', + 'dustbinremoved', + 'unabletomove', + 'lidarblocked', + 'unabletofinddock', + 'batterylow', + 'unknowninternal', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Error', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vacuum_error', + 'unique_id': '123456789ABCDEFGH_vacuum_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'my_device Error', + 'options': list([ + 'ok', + 'sidebrushstuck', + 'mainbrushstuck', + 'wheelblocked', + 'trapped', + 'trappedcliff', + 'dustbinremoved', + 'unabletomove', + 'lidarblocked', + 'unabletofinddock', + 'batterylow', + 'unknowninternal', + ]), + }), + 'context': , + 'entity_id': 'sensor.my_device_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- # name: test_states[sensor.my_device_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 7adda900c02..f22f8d0cd36 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -219,6 +219,52 @@ 'state': 'on', }) # --- +# name: test_states[switch.my_device_carpet_boost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_carpet_boost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Carpet boost', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'carpet_boost', + 'unique_id': '123456789ABCDEFGH_carpet_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_carpet_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Carpet boost', + }), + 'context': , + 'entity_id': 'switch.my_device_carpet_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_states[switch.my_device_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({