Skip to content

Commit 032cd5d

Browse files
rytilahtisdb9696
andauthored
Improve overheat reporting (#1335)
Different devices and different firmwares report overheated status in different ways. Some devices indicate support for `overheat_protect` component, but there are devices that report `overheat_status` even when it is not listed. Some other devices use `overheated` boolean that was already previously supported, but this PR adds support for much more devices that use `overheat_status` for reporting. The "overheated" feature is moved into its own module, and uses either of the ways to report this information. This will also rename `REQUIRED_KEY_ON_PARENT` to `SYSINFO_LOOKUP_KEYS` and change its logic to check if any of the keys in the list are found in the sysinfo. ``` tpr@lumipyry ~/c/p/tests (fix/overheated)> ag 'overheat_protect' -c|wc -l 15 tpr@lumipyry ~/c/p/tests (fix/overheated)> ag 'overheated' -c|wc -l 38 tpr@lumipyry ~/c/p/tests (fix/overheated)> ag 'overheat_status' -c|wc -l 20 ``` --------- Co-authored-by: Steven B. <51370195+sdb9696@users.noreply.github.com>
1 parent bf8f0ad commit 032cd5d

File tree

8 files changed

+108
-21
lines changed

8 files changed

+108
-21
lines changed

docs/tutorial.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,5 @@
9191
True
9292
>>> for feat in dev.features.values():
9393
>>> print(f"{feat.name}: {feat.value}")
94-
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nReboot: <Action>\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: None\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: None\nCheck latest firmware: <Action>\nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nDevice time: 2024-02-23 02:40:15+01:00
94+
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nReboot: <Action>\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: None\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: None\nCheck latest firmware: <Action>\nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nOverheated: False\nDevice time: 2024-02-23 02:40:15+01:00
9595
"""

kasa/feature.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
Signal Level (signal_level): 2
2525
RSSI (rssi): -52
2626
SSID (ssid): #MASKED_SSID#
27-
Overheated (overheated): False
2827
Reboot (reboot): <Action>
2928
Brightness (brightness): 100
3029
Cloud connection (cloud_connection): True
@@ -39,6 +38,7 @@
3938
Light preset (light_preset): Not set
4039
Smooth transition on (smooth_transition_on): 2
4140
Smooth transition off (smooth_transition_off): 2
41+
Overheated (overheated): False
4242
Device time (device_time): 2024-02-23 02:40:15+01:00
4343
4444
To see whether a device supports a feature, check for the existence of it:

kasa/smart/modules/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .lightstripeffect import LightStripEffect
2525
from .lighttransition import LightTransition
2626
from .motionsensor import MotionSensor
27+
from .overheatprotection import OverheatProtection
2728
from .reportmode import ReportMode
2829
from .temperaturecontrol import TemperatureControl
2930
from .temperaturesensor import TemperatureSensor
@@ -64,4 +65,5 @@
6465
"FrostProtection",
6566
"Thermostat",
6667
"SmartLightEffect",
68+
"OverheatProtection",
6769
]

kasa/smart/modules/contactsensor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ContactSensor(SmartModule):
1010
"""Implementation of contact sensor module."""
1111

1212
REQUIRED_COMPONENT = None # we depend on availability of key
13-
REQUIRED_KEY_ON_PARENT = "open"
13+
SYSINFO_LOOKUP_KEYS = ["open"]
1414

1515
def _initialize_features(self) -> None:
1616
"""Initialize features after the initial update."""
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Overheat module."""
2+
3+
from __future__ import annotations
4+
5+
from ...feature import Feature
6+
from ..smartmodule import SmartModule
7+
8+
9+
class OverheatProtection(SmartModule):
10+
"""Implementation for overheat_protection."""
11+
12+
SYSINFO_LOOKUP_KEYS = ["overheated", "overheat_status"]
13+
14+
def _initialize_features(self) -> None:
15+
"""Initialize features after the initial update."""
16+
self._add_feature(
17+
Feature(
18+
self._device,
19+
container=self,
20+
id="overheated",
21+
name="Overheated",
22+
attribute_getter="overheated",
23+
icon="mdi:heat-wave",
24+
type=Feature.Type.BinarySensor,
25+
category=Feature.Category.Info,
26+
)
27+
)
28+
29+
@property
30+
def overheated(self) -> bool:
31+
"""Return True if device reports overheating."""
32+
if (value := self._device.sys_info.get("overheat_status")) is not None:
33+
# Value can be normal, cooldown, or overheated.
34+
# We report all but normal as overheated.
35+
return value != "normal"
36+
37+
return self._device.sys_info["overheated"]
38+
39+
def query(self) -> dict:
40+
"""Query to execute during the update cycle."""
41+
return {}

kasa/smart/smartdevice.py

+2-16
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,8 @@ async def _initialize_modules(self) -> None:
349349
) or mod.__name__ in child_modules_to_skip:
350350
continue
351351
required_component = cast(str, mod.REQUIRED_COMPONENT)
352-
if required_component in self._components or (
353-
mod.REQUIRED_KEY_ON_PARENT
354-
and self.sys_info.get(mod.REQUIRED_KEY_ON_PARENT) is not None
352+
if required_component in self._components or any(
353+
self.sys_info.get(key) is not None for key in mod.SYSINFO_LOOKUP_KEYS
355354
):
356355
_LOGGER.debug(
357356
"Device %s, found required %s, adding %s to modules.",
@@ -440,19 +439,6 @@ async def _initialize_features(self) -> None:
440439
)
441440
)
442441

443-
if "overheated" in self._info:
444-
self._add_feature(
445-
Feature(
446-
self,
447-
id="overheated",
448-
name="Overheated",
449-
attribute_getter=lambda x: x._info["overheated"],
450-
icon="mdi:heat-wave",
451-
type=Feature.Type.BinarySensor,
452-
category=Feature.Category.Info,
453-
)
454-
)
455-
456442
# We check for the key available, and not for the property truthiness,
457443
# as the value is falsy when the device is off.
458444
if "on_time" in self._info:

kasa/smart/smartmodule.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ class SmartModule(Module):
5454
NAME: str
5555
#: Module is initialized, if the given component is available
5656
REQUIRED_COMPONENT: str | None = None
57-
#: Module is initialized, if the given key available in the main sysinfo
58-
REQUIRED_KEY_ON_PARENT: str | None = None
57+
#: Module is initialized, if any of the given keys exists in the sysinfo
58+
SYSINFO_LOOKUP_KEYS: list[str] = []
5959
#: Query to execute during the main update cycle
6060
QUERY_GETTER_NAME: str
6161

tests/smart/test_smartdevice.py

+58
Original file line numberDiff line numberDiff line change
@@ -470,3 +470,61 @@ async def test_smart_temp_range(dev: Device):
470470
light = dev.modules.get(Module.Light)
471471
assert light
472472
assert light.valid_temperature_range
473+
474+
475+
@device_smart
476+
async def test_initialize_modules_sysinfo_lookup_keys(
477+
dev: SmartDevice, mocker: MockerFixture
478+
):
479+
"""Test that matching modules using SYSINFO_LOOKUP_KEYS are initialized correctly."""
480+
481+
class AvailableKey(SmartModule):
482+
SYSINFO_LOOKUP_KEYS = ["device_id"]
483+
484+
class NonExistingKey(SmartModule):
485+
SYSINFO_LOOKUP_KEYS = ["this_does_not_exist"]
486+
487+
# The __init_subclass__ hook in smartmodule checks the path,
488+
# so we have to manually add these for testing.
489+
mocker.patch.dict(
490+
"kasa.smart.smartmodule.SmartModule.REGISTERED_MODULES",
491+
{
492+
AvailableKey._module_name(): AvailableKey,
493+
NonExistingKey._module_name(): NonExistingKey,
494+
},
495+
)
496+
497+
# We have an already initialized device, so we try to initialize the modules again
498+
await dev._initialize_modules()
499+
500+
assert "AvailableKey" in dev.modules
501+
assert "NonExistingKey" not in dev.modules
502+
503+
504+
@device_smart
505+
async def test_initialize_modules_required_component(
506+
dev: SmartDevice, mocker: MockerFixture
507+
):
508+
"""Test that matching modules using REQUIRED_COMPONENT are initialized correctly."""
509+
510+
class AvailableComponent(SmartModule):
511+
REQUIRED_COMPONENT = "device"
512+
513+
class NonExistingComponent(SmartModule):
514+
REQUIRED_COMPONENT = "this_does_not_exist"
515+
516+
# The __init_subclass__ hook in smartmodule checks the path,
517+
# so we have to manually add these for testing.
518+
mocker.patch.dict(
519+
"kasa.smart.smartmodule.SmartModule.REGISTERED_MODULES",
520+
{
521+
AvailableComponent._module_name(): AvailableComponent,
522+
NonExistingComponent._module_name(): NonExistingComponent,
523+
},
524+
)
525+
526+
# We have an already initialized device, so we try to initialize the modules again
527+
await dev._initialize_modules()
528+
529+
assert "AvailableComponent" in dev.modules
530+
assert "NonExistingComponent" not in dev.modules

0 commit comments

Comments
 (0)