Handle incomplete power consumption reports in SmartThings (#140370)

This commit is contained in:
Joost Lekkerkerker 2025-03-11 14:10:06 +01:00 committed by GitHub
parent 13e9906929
commit 0e7a083847
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 337 additions and 28 deletions

View File

@ -228,28 +228,6 @@ KEEP_CAPABILITY_QUIRK: dict[
Capability.DEMAND_RESPONSE_LOAD_CONTROL: lambda _: True,
}
POWER_CONSUMPTION_FIELDS = {
"energy",
"power",
"deltaEnergy",
"powerEnergy",
"energySaved",
}
CAPABILITY_VALIDATION: dict[
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
] = {
Capability.POWER_CONSUMPTION_REPORT: (
lambda status: (
(power_consumption := status[Attribute.POWER_CONSUMPTION].value) is not None
and all(
field in cast(dict, power_consumption)
for field in POWER_CONSUMPTION_FIELDS
)
)
)
}
def process_status(
status: dict[str, dict[Capability | str, dict[Attribute | str, Status]]],
@ -273,8 +251,4 @@ def process_status(
or not KEEP_CAPABILITY_QUIRK[capability](main_component[capability])
):
del main_component[capability]
for capability in list(main_component):
if capability in CAPABILITY_VALIDATION:
if not CAPABILITY_VALIDATION[capability](main_component[capability]):
del main_component[capability]
return status

View File

@ -5,9 +5,9 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import Any, cast
from pysmartthings import Attribute, Capability, SmartThings
from pysmartthings import Attribute, Capability, SmartThings, Status
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -131,6 +131,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
unique_id_separator: str = "."
capability_ignore_list: list[set[Capability]] | None = None
options_attribute: Attribute | None = None
exists_fn: Callable[[Status], bool] | None = None
CAPABILITY_TO_SENSORS: dict[
@ -583,6 +584,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energy" in value
),
),
SmartThingsSensorEntityDescription(
key="power_meter",
@ -592,6 +597,10 @@ CAPABILITY_TO_SENSORS: dict[
value_fn=lambda value: value["power"],
extra_state_attributes_fn=power_attributes,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "power" in value
),
),
SmartThingsSensorEntityDescription(
key="deltaEnergy_meter",
@ -601,6 +610,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["deltaEnergy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "deltaEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="powerEnergy_meter",
@ -610,6 +623,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["powerEnergy"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "powerEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="energySaved_meter",
@ -619,6 +636,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energySaved"] / 1000,
suggested_display_precision=2,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energySaved" in value
),
),
]
},
@ -980,6 +1001,10 @@ async def async_setup_entry(
for capability_list in description.capability_ignore_list
)
)
and (
not description.exists_fn
or description.exists_fn(device.status[MAIN][capability][attribute])
)
)

View File

@ -127,6 +127,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"generic_ef00_v1",
"bosch_radiator_thermostat_ii",
"im_speaker_ai_0001",
"tplink_p110",
]
)
def device_fixture(

View File

@ -0,0 +1,46 @@
{
"components": {
"main": {
"powerConsumptionReport": {
"powerConsumption": {
"value": {
"start": "2025-03-10T14:43:42.500Z",
"end": "2025-03-10T14:59:42.500Z",
"energy": 15720,
"deltaEnergy": 0
},
"timestamp": "2025-03-10T14:59:50.010Z"
}
},
"healthCheck": {
"checkInterval": {
"value": 60,
"unit": "s",
"data": {
"deviceScheme": "UNTRACKED",
"protocol": "cloud"
},
"timestamp": "2024-03-07T21:14:59.839Z"
},
"healthStatus": {
"value": null
},
"DeviceWatch-Enroll": {
"value": null
},
"DeviceWatch-DeviceStatus": {
"value": "online",
"data": {},
"timestamp": "2025-03-10T14:14:37.232Z"
}
},
"refresh": {},
"switch": {
"switch": {
"value": "on",
"timestamp": "2025-03-10T14:14:37.232Z"
}
}
}
}
}

View File

@ -0,0 +1,73 @@
{
"items": [
{
"deviceId": "6602696a-1e48-49e4-919f-69406f5b5da1",
"name": "plug-energy-usage-report",
"label": "Sp\u00fclmaschine",
"manufacturerName": "0AI2",
"presentationId": "ST_8f2be0ec-1113-46e0-ad56-3e92eb27410f",
"deviceManufacturerCode": "TP-Link",
"locationId": "70da36b0-bd25-410c-beed-7f0dbf658448",
"ownerId": "be5d4173-dd49-1eee-56f5-f98306ee872c",
"roomId": "bd13616d-b7e2-44ff-914c-eb38ea18c4b4",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "healthCheck",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "powerConsumptionReport",
"version": 1
}
],
"categories": [
{
"name": "SmartPlug",
"categoryType": "manufacturer"
},
{
"name": "SmartPlug",
"categoryType": "user"
}
]
}
],
"createTime": "2024-03-07T21:14:59.762Z",
"profile": {
"id": "a25b207e-cbb9-40ae-8a88-906637c22ab6"
},
"viper": {
"uniqueIdentifier": "8022F7F6FE0A6EACA52B5D89C0D667352136D8C6",
"manufacturerName": "TP-Link",
"modelName": "P110",
"swVersion": "1.3.1 Build 240621 Rel.162048",
"hwVersion": "1.0",
"endpointAppId": "viper_7ea6bb80-b876-11eb-be42-952f31ab3f7b"
},
"type": "VIPER",
"restrictionTier": 0,
"allowed": null,
"indoorMap": {
"coordinates": [0.0, 0.0, 0.0],
"rotation": [0.0, 180.0, 0.0],
"visible": false,
"data": null
},
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -1124,6 +1124,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[tplink_p110]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': '1.0',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'6602696a-1e48-49e4-919f-69406f5b5da1',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'TP-Link',
'model': 'P110',
'model_id': None,
'name': 'Spülmaschine',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '1.3.1 Build 240621 Rel.162048',
'via_device_id': None,
})
# ---
# name: test_devices[vd_network_audio_002s]
DeviceRegistryEntrySnapshot({
'area_id': 'theater',

View File

@ -6212,6 +6212,116 @@
'state': '15',
})
# ---
# name: test_all_entities[tplink_p110][sensor.spulmaschine_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.spulmaschine_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '6602696a-1e48-49e4-919f-69406f5b5da1.energy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[tplink_p110][sensor.spulmaschine_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Spülmaschine Energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.spulmaschine_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '15.72',
})
# ---
# name: test_all_entities[tplink_p110][sensor.spulmaschine_energy_difference-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.spulmaschine_energy_difference',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy difference',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'energy_difference',
'unique_id': '6602696a-1e48-49e4-919f-69406f5b5da1.deltaEnergy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[tplink_p110][sensor.spulmaschine_energy_difference-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Spülmaschine Energy difference',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.spulmaschine_energy_difference',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_all_entities[vd_network_audio_002s][sensor.soundbar_living_media_playback_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -516,6 +516,53 @@
'state': 'on',
})
# ---
# name: test_all_entities[tplink_p110][switch.spulmaschine-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.spulmaschine',
'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': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '6602696a-1e48-49e4-919f-69406f5b5da1',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[tplink_p110][switch.spulmaschine-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Spülmaschine',
}),
'context': <ANY>,
'entity_id': 'switch.spulmaschine',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[vd_network_audio_002s][switch.soundbar_living-entry]
EntityRegistryEntrySnapshot({
'aliases': set({