Skip to content
Snippets Groups Projects
Unverified Commit 0e7a0838 authored by Joost Lekkerkerker's avatar Joost Lekkerkerker Committed by GitHub
Browse files

Handle incomplete power consumption reports in SmartThings (#140370)

parent 13e99069
No related branches found
No related tags found
No related merge requests found
......@@ -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
......@@ -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])
)
)
......
......@@ -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(
......
{
"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"
}
}
}
}
}
{
"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": {}
}
......@@ -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',
......
......@@ -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({
......
......@@ -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({
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment