Skip to content

Commit 8541dc5

Browse files
joostlekfrenck
authored andcommitted
Handle incomplete power consumption reports in SmartThings (#140370)
1 parent 5327996 commit 8541dc5

File tree

8 files changed

+337
-28
lines changed

8 files changed

+337
-28
lines changed

homeassistant/components/smartthings/__init__.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -203,28 +203,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
203203
Capability.DEMAND_RESPONSE_LOAD_CONTROL: lambda _: True,
204204
}
205205

206-
POWER_CONSUMPTION_FIELDS = {
207-
"energy",
208-
"power",
209-
"deltaEnergy",
210-
"powerEnergy",
211-
"energySaved",
212-
}
213-
214-
CAPABILITY_VALIDATION: dict[
215-
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
216-
] = {
217-
Capability.POWER_CONSUMPTION_REPORT: (
218-
lambda status: (
219-
(power_consumption := status[Attribute.POWER_CONSUMPTION].value) is not None
220-
and all(
221-
field in cast(dict, power_consumption)
222-
for field in POWER_CONSUMPTION_FIELDS
223-
)
224-
)
225-
)
226-
}
227-
228206

229207
def process_status(
230208
status: dict[str, dict[Capability | str, dict[Attribute | str, Status]]],
@@ -248,8 +226,4 @@ def process_status(
248226
or not KEEP_CAPABILITY_QUIRK[capability](main_component[capability])
249227
):
250228
del main_component[capability]
251-
for capability in list(main_component):
252-
if capability in CAPABILITY_VALIDATION:
253-
if not CAPABILITY_VALIDATION[capability](main_component[capability]):
254-
del main_component[capability]
255229
return status

homeassistant/components/smartthings/sensor.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from collections.abc import Callable, Mapping
66
from dataclasses import dataclass
77
from datetime import datetime
8-
from typing import Any
8+
from typing import Any, cast
99

10-
from pysmartthings import Attribute, Capability, SmartThings
10+
from pysmartthings import Attribute, Capability, SmartThings, Status
1111

1212
from homeassistant.components.sensor import (
1313
SensorDeviceClass,
@@ -131,6 +131,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
131131
unique_id_separator: str = "."
132132
capability_ignore_list: list[set[Capability]] | None = None
133133
options_attribute: Attribute | None = None
134+
exists_fn: Callable[[Status], bool] | None = None
134135

135136

136137
CAPABILITY_TO_SENSORS: dict[
@@ -583,6 +584,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
583584
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
584585
value_fn=lambda value: value["energy"] / 1000,
585586
suggested_display_precision=2,
587+
exists_fn=lambda status: (
588+
(value := cast(dict | None, status.value)) is not None
589+
and "energy" in value
590+
),
586591
),
587592
SmartThingsSensorEntityDescription(
588593
key="power_meter",
@@ -592,6 +597,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
592597
value_fn=lambda value: value["power"],
593598
extra_state_attributes_fn=power_attributes,
594599
suggested_display_precision=2,
600+
exists_fn=lambda status: (
601+
(value := cast(dict | None, status.value)) is not None
602+
and "power" in value
603+
),
595604
),
596605
SmartThingsSensorEntityDescription(
597606
key="deltaEnergy_meter",
@@ -601,6 +610,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
601610
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
602611
value_fn=lambda value: value["deltaEnergy"] / 1000,
603612
suggested_display_precision=2,
613+
exists_fn=lambda status: (
614+
(value := cast(dict | None, status.value)) is not None
615+
and "deltaEnergy" in value
616+
),
604617
),
605618
SmartThingsSensorEntityDescription(
606619
key="powerEnergy_meter",
@@ -610,6 +623,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
610623
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
611624
value_fn=lambda value: value["powerEnergy"] / 1000,
612625
suggested_display_precision=2,
626+
exists_fn=lambda status: (
627+
(value := cast(dict | None, status.value)) is not None
628+
and "powerEnergy" in value
629+
),
613630
),
614631
SmartThingsSensorEntityDescription(
615632
key="energySaved_meter",
@@ -619,6 +636,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
619636
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
620637
value_fn=lambda value: value["energySaved"] / 1000,
621638
suggested_display_precision=2,
639+
exists_fn=lambda status: (
640+
(value := cast(dict | None, status.value)) is not None
641+
and "energySaved" in value
642+
),
622643
),
623644
]
624645
},
@@ -973,6 +994,10 @@ async def async_setup_entry(
973994
for capability_list in description.capability_ignore_list
974995
)
975996
)
997+
and (
998+
not description.exists_fn
999+
or description.exists_fn(device.status[MAIN][capability][attribute])
1000+
)
9761001
)
9771002

9781003

tests/components/smartthings/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
123123
"generic_ef00_v1",
124124
"bosch_radiator_thermostat_ii",
125125
"im_speaker_ai_0001",
126+
"tplink_p110",
126127
]
127128
)
128129
def device_fixture(
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"components": {
3+
"main": {
4+
"powerConsumptionReport": {
5+
"powerConsumption": {
6+
"value": {
7+
"start": "2025-03-10T14:43:42.500Z",
8+
"end": "2025-03-10T14:59:42.500Z",
9+
"energy": 15720,
10+
"deltaEnergy": 0
11+
},
12+
"timestamp": "2025-03-10T14:59:50.010Z"
13+
}
14+
},
15+
"healthCheck": {
16+
"checkInterval": {
17+
"value": 60,
18+
"unit": "s",
19+
"data": {
20+
"deviceScheme": "UNTRACKED",
21+
"protocol": "cloud"
22+
},
23+
"timestamp": "2024-03-07T21:14:59.839Z"
24+
},
25+
"healthStatus": {
26+
"value": null
27+
},
28+
"DeviceWatch-Enroll": {
29+
"value": null
30+
},
31+
"DeviceWatch-DeviceStatus": {
32+
"value": "online",
33+
"data": {},
34+
"timestamp": "2025-03-10T14:14:37.232Z"
35+
}
36+
},
37+
"refresh": {},
38+
"switch": {
39+
"switch": {
40+
"value": "on",
41+
"timestamp": "2025-03-10T14:14:37.232Z"
42+
}
43+
}
44+
}
45+
}
46+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"items": [
3+
{
4+
"deviceId": "6602696a-1e48-49e4-919f-69406f5b5da1",
5+
"name": "plug-energy-usage-report",
6+
"label": "Sp\u00fclmaschine",
7+
"manufacturerName": "0AI2",
8+
"presentationId": "ST_8f2be0ec-1113-46e0-ad56-3e92eb27410f",
9+
"deviceManufacturerCode": "TP-Link",
10+
"locationId": "70da36b0-bd25-410c-beed-7f0dbf658448",
11+
"ownerId": "be5d4173-dd49-1eee-56f5-f98306ee872c",
12+
"roomId": "bd13616d-b7e2-44ff-914c-eb38ea18c4b4",
13+
"components": [
14+
{
15+
"id": "main",
16+
"label": "main",
17+
"capabilities": [
18+
{
19+
"id": "healthCheck",
20+
"version": 1
21+
},
22+
{
23+
"id": "refresh",
24+
"version": 1
25+
},
26+
{
27+
"id": "switch",
28+
"version": 1
29+
},
30+
{
31+
"id": "powerConsumptionReport",
32+
"version": 1
33+
}
34+
],
35+
"categories": [
36+
{
37+
"name": "SmartPlug",
38+
"categoryType": "manufacturer"
39+
},
40+
{
41+
"name": "SmartPlug",
42+
"categoryType": "user"
43+
}
44+
]
45+
}
46+
],
47+
"createTime": "2024-03-07T21:14:59.762Z",
48+
"profile": {
49+
"id": "a25b207e-cbb9-40ae-8a88-906637c22ab6"
50+
},
51+
"viper": {
52+
"uniqueIdentifier": "8022F7F6FE0A6EACA52B5D89C0D667352136D8C6",
53+
"manufacturerName": "TP-Link",
54+
"modelName": "P110",
55+
"swVersion": "1.3.1 Build 240621 Rel.162048",
56+
"hwVersion": "1.0",
57+
"endpointAppId": "viper_7ea6bb80-b876-11eb-be42-952f31ab3f7b"
58+
},
59+
"type": "VIPER",
60+
"restrictionTier": 0,
61+
"allowed": null,
62+
"indoorMap": {
63+
"coordinates": [0.0, 0.0, 0.0],
64+
"rotation": [0.0, 180.0, 0.0],
65+
"visible": false,
66+
"data": null
67+
},
68+
"executionContext": "CLOUD",
69+
"relationships": []
70+
}
71+
],
72+
"_links": {}
73+
}

tests/components/smartthings/snapshots/test_init.ambr

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,39 @@
11241124
'via_device_id': None,
11251125
})
11261126
# ---
1127+
# name: test_devices[tplink_p110]
1128+
DeviceRegistryEntrySnapshot({
1129+
'area_id': None,
1130+
'config_entries': <ANY>,
1131+
'config_entries_subentries': <ANY>,
1132+
'configuration_url': 'https://account.smartthings.com',
1133+
'connections': set({
1134+
}),
1135+
'disabled_by': None,
1136+
'entry_type': None,
1137+
'hw_version': '1.0',
1138+
'id': <ANY>,
1139+
'identifiers': set({
1140+
tuple(
1141+
'smartthings',
1142+
'6602696a-1e48-49e4-919f-69406f5b5da1',
1143+
),
1144+
}),
1145+
'is_new': False,
1146+
'labels': set({
1147+
}),
1148+
'manufacturer': 'TP-Link',
1149+
'model': 'P110',
1150+
'model_id': None,
1151+
'name': 'Spülmaschine',
1152+
'name_by_user': None,
1153+
'primary_config_entry': <ANY>,
1154+
'serial_number': None,
1155+
'suggested_area': None,
1156+
'sw_version': '1.3.1 Build 240621 Rel.162048',
1157+
'via_device_id': None,
1158+
})
1159+
# ---
11271160
# name: test_devices[vd_network_audio_002s]
11281161
DeviceRegistryEntrySnapshot({
11291162
'area_id': None,

0 commit comments

Comments
 (0)