Add more vacuum features for tplink (#136580)

This commit is contained in:
Teemu R. 2025-01-29 01:23:29 +01:00 committed by GitHub
parent e07e8b8706
commit c2cbbf1e1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 252 additions and 1 deletions

View File

@ -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"
}
}
},

View File

@ -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}

View File

@ -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

View File

@ -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"
}
}
},

View File

@ -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}

View File

@ -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"

View File

@ -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": "<Action>",
"type": "Action",

View File

@ -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': <NumberMode.SLIDER: 'slider'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.my_device_clean_count',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <NumberMode.SLIDER: 'slider'>,
'step': 1.0,
}),
'context': <ANY>,
'entity_id': 'number.my_device_clean_count',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_states[number.my_device_pan_degrees-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'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': <ANY>,
'entity_id': 'sensor.my_device_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'ok',
})
# ---
# name: test_states[sensor.my_device_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.my_device_carpet_boost',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <ANY>,
'entity_id': 'switch.my_device_carpet_boost',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_states[switch.my_device_child_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': set({