Add cleaning statistics for tplink (#135784)

Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com>
This commit is contained in:
Teemu R. 2025-01-29 15:56:47 +01:00 committed by GitHub
parent c7176f6849
commit 653ff47171
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 497 additions and 3 deletions

View File

@ -4,7 +4,9 @@ from __future__ import annotations
from typing import Final
from homeassistant.const import Platform, UnitOfTemperature
from kasa.smart.modules.clean import AreaUnit
from homeassistant.const import Platform, UnitOfArea, UnitOfTemperature
DOMAIN = "tplink"
@ -47,4 +49,6 @@ PLATFORMS: Final = [
UNIT_MAPPING = {
"celsius": UnitOfTemperature.CELSIUS,
"fahrenheit": UnitOfTemperature.FAHRENHEIT,
AreaUnit.Sqm: UnitOfArea.SQUARE_METERS,
AreaUnit.Sqft: UnitOfArea.SQUARE_FEET,
}

View File

@ -124,6 +124,50 @@ SENSOR_DESCRIPTIONS: tuple[TPLinkSensorEntityDescription, ...] = (
TPLinkSensorEntityDescription(
key="alarm_source",
),
# Vacuum cleaning records
TPLinkSensorEntityDescription(
key="clean_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
),
TPLinkSensorEntityDescription(
key="clean_area",
device_class=SensorDeviceClass.AREA,
),
TPLinkSensorEntityDescription(
key="clean_progress",
),
TPLinkSensorEntityDescription(
key="last_clean_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
),
TPLinkSensorEntityDescription(
key="last_clean_area",
device_class=SensorDeviceClass.AREA,
),
TPLinkSensorEntityDescription(
key="last_clean_timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
),
TPLinkSensorEntityDescription(
key="total_clean_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
convert_fn=_TOTAL_SECONDS_METHOD_CALLER,
),
TPLinkSensorEntityDescription(
key="total_clean_area",
device_class=SensorDeviceClass.AREA,
),
TPLinkSensorEntityDescription(
key="total_clean_count",
),
TPLinkSensorEntityDescription(
key="main_brush_remaining",
device_class=SensorDeviceClass.DURATION,

View File

@ -217,6 +217,33 @@
"alarm_source": {
"name": "Alarm source"
},
"clean_area": {
"name": "Cleaning area"
},
"clean_time": {
"name": "Cleaning time"
},
"clean_progress": {
"name": "Cleaning progress"
},
"total_clean_area": {
"name": "Total cleaning area"
},
"total_clean_time": {
"name": "Total cleaning time"
},
"total_clean_count": {
"name": "Total cleaning count"
},
"last_clean_area": {
"name": "Last cleaned area"
},
"last_clean_time": {
"name": "Last cleaned time"
},
"last_clean_timestamp": {
"name": "Last clean start"
},
"main_brush_remaining": {
"name": "Main brush remaining"
},

View File

@ -18,7 +18,7 @@ from kasa import (
from kasa.interfaces import Fan, Light, LightEffect, LightState, Thermostat
from kasa.smart.modules import Speaker
from kasa.smart.modules.alarm import Alarm
from kasa.smart.modules.clean import Clean, ErrorCode, Status
from kasa.smart.modules.clean import AreaUnit, Clean, ErrorCode, Status
from kasa.smartcam.modules.camera import LOCAL_STREAMING_PORT, Camera
from syrupy import SnapshotAssertion
@ -60,7 +60,7 @@ def _load_feature_fixtures():
FEATURES_FIXTURE = _load_feature_fixtures()
FIXTURE_ENUM_TYPES = {"CleanErrorCode": ErrorCode}
FIXTURE_ENUM_TYPES = {"CleanErrorCode": ErrorCode, "CleanAreaUnit": AreaUnit}
async def setup_platform_for_device(
@ -276,12 +276,17 @@ 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
if timedelta_type := fixture.get("timedelta_type"):
fixture["value"] = timedelta(**{timedelta_type: fixture["value"]})
if unit_enum_type := fixture.get("unit_enum_type"):
val = FIXTURE_ENUM_TYPES[unit_enum_type](fixture["unit"])
fixture["unit"] = val
else:
assert require_fixture is False, (
f"No fixture defined for feature {id} and require_fixture is True"

View File

@ -341,6 +341,61 @@
"Connection 2"
]
},
"clean_time": {
"type": "Sensor",
"category": "Info",
"value": 12,
"timedelta_type": "minutes"
},
"clean_area": {
"type": "Sensor",
"category": "Info",
"value": 2,
"unit": 1,
"unit_enum_type": "CleanAreaUnit"
},
"clean_progress": {
"type": "Sensor",
"category": "Info",
"value": 30,
"unit": "%"
},
"total_clean_time": {
"type": "Sensor",
"category": "Debug",
"value": 120,
"timedelta_type": "minutes"
},
"total_clean_area": {
"type": "Sensor",
"category": "Debug",
"value": 2,
"unit": 1,
"unit_enum_type": "CleanAreaUnit"
},
"last_clean_time": {
"type": "Sensor",
"category": "Debug",
"value": 60,
"timedelta_type": "minutes"
},
"last_clean_area": {
"type": "Sensor",
"category": "Debug",
"value": 2,
"unit": 1,
"unit_enum_type": "CleanAreaUnit"
},
"last_clean_timestamp": {
"type": "Sensor",
"category": "Debug",
"value": "2024-06-24 10:03:11.046643+01:00"
},
"total_clean_count": {
"type": "Sensor",
"category": "Debug",
"value": 12
},
"alarm_volume": {
"value": "normal",
"type": "Choice",

View File

@ -238,6 +238,155 @@
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
})
# ---
# name: test_states[sensor.my_device_cleaning_area-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'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_cleaning_area',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
}),
'original_device_class': <SensorDeviceClass.AREA: 'area'>,
'original_icon': None,
'original_name': 'Cleaning area',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'clean_area',
'unique_id': '123456789ABCDEFGH_clean_area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_states[sensor.my_device_cleaning_area-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'area',
'friendly_name': 'my_device Cleaning area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.my_device_cleaning_area',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.2',
})
# ---
# name: test_states[sensor.my_device_cleaning_progress-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'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_cleaning_progress',
'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': 'Cleaning progress',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'clean_progress',
'unique_id': '123456789ABCDEFGH_clean_progress',
'unit_of_measurement': '%',
})
# ---
# name: test_states[sensor.my_device_cleaning_progress-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'my_device Cleaning progress',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.my_device_cleaning_progress',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30',
})
# ---
# name: test_states[sensor.my_device_cleaning_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'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_cleaning_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Cleaning time',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'clean_time',
'unique_id': '123456789ABCDEFGH_clean_time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_states[sensor.my_device_cleaning_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'my_device Cleaning time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.my_device_cleaning_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '12.00',
})
# ---
# name: test_states[sensor.my_device_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -578,6 +727,111 @@
'state': '12',
})
# ---
# name: test_states[sensor.my_device_last_clean_start-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_last_clean_start',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Last clean start',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'last_clean_timestamp',
'unique_id': '123456789ABCDEFGH_last_clean_timestamp',
'unit_of_measurement': None,
})
# ---
# name: test_states[sensor.my_device_last_cleaned_area-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_last_cleaned_area',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
}),
'original_device_class': <SensorDeviceClass.AREA: 'area'>,
'original_icon': None,
'original_name': 'Last cleaned area',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'last_clean_area',
'unique_id': '123456789ABCDEFGH_last_clean_area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_states[sensor.my_device_last_cleaned_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_last_cleaned_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Last cleaned time',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'last_clean_time',
'unique_id': '123456789ABCDEFGH_last_clean_time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_states[sensor.my_device_last_water_leak_alert-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -1167,6 +1421,111 @@
'state': '5.23',
})
# ---
# name: test_states[sensor.my_device_total_cleaning_area-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_total_cleaning_area',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
}),
'original_device_class': <SensorDeviceClass.AREA: 'area'>,
'original_icon': None,
'original_name': 'Total cleaning area',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_clean_area',
'unique_id': '123456789ABCDEFGH_total_clean_area',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_states[sensor.my_device_total_cleaning_count-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_total_cleaning_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': 'Total cleaning count',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_clean_count',
'unique_id': '123456789ABCDEFGH_total_clean_count',
'unit_of_measurement': None,
})
# ---
# name: test_states[sensor.my_device_total_cleaning_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.my_device_total_cleaning_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Total cleaning time',
'platform': 'tplink',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_clean_time',
'unique_id': '123456789ABCDEFGH_total_clean_time',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_states[sensor.my_device_total_consumption-entry]
EntityRegistryEntrySnapshot({
'aliases': set({