mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
Improve overheat reporting (#1335)
Some checks are pending
CI / Perform linting checks (3.13) (push) Waiting to run
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.13) (push) Blocked by required conditions
CodeQL checks / Analyze (python) (push) Waiting to run
Some checks are pending
CI / Perform linting checks (3.13) (push) Waiting to run
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.13) (push) Blocked by required conditions
CodeQL checks / Analyze (python) (push) Waiting to run
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>
This commit is contained in:
parent
bf8f0adabe
commit
032cd5d2cc
@ -91,5 +91,5 @@ False
|
|||||||
True
|
True
|
||||||
>>> for feat in dev.features.values():
|
>>> for feat in dev.features.values():
|
||||||
>>> print(f"{feat.name}: {feat.value}")
|
>>> print(f"{feat.name}: {feat.value}")
|
||||||
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
|
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
|
||||||
"""
|
"""
|
||||||
|
@ -24,7 +24,6 @@ State (state): True
|
|||||||
Signal Level (signal_level): 2
|
Signal Level (signal_level): 2
|
||||||
RSSI (rssi): -52
|
RSSI (rssi): -52
|
||||||
SSID (ssid): #MASKED_SSID#
|
SSID (ssid): #MASKED_SSID#
|
||||||
Overheated (overheated): False
|
|
||||||
Reboot (reboot): <Action>
|
Reboot (reboot): <Action>
|
||||||
Brightness (brightness): 100
|
Brightness (brightness): 100
|
||||||
Cloud connection (cloud_connection): True
|
Cloud connection (cloud_connection): True
|
||||||
@ -39,6 +38,7 @@ Light effect (light_effect): Off
|
|||||||
Light preset (light_preset): Not set
|
Light preset (light_preset): Not set
|
||||||
Smooth transition on (smooth_transition_on): 2
|
Smooth transition on (smooth_transition_on): 2
|
||||||
Smooth transition off (smooth_transition_off): 2
|
Smooth transition off (smooth_transition_off): 2
|
||||||
|
Overheated (overheated): False
|
||||||
Device time (device_time): 2024-02-23 02:40:15+01:00
|
Device time (device_time): 2024-02-23 02:40:15+01:00
|
||||||
|
|
||||||
To see whether a device supports a feature, check for the existence of it:
|
To see whether a device supports a feature, check for the existence of it:
|
||||||
|
@ -24,6 +24,7 @@ from .lightpreset import LightPreset
|
|||||||
from .lightstripeffect import LightStripEffect
|
from .lightstripeffect import LightStripEffect
|
||||||
from .lighttransition import LightTransition
|
from .lighttransition import LightTransition
|
||||||
from .motionsensor import MotionSensor
|
from .motionsensor import MotionSensor
|
||||||
|
from .overheatprotection import OverheatProtection
|
||||||
from .reportmode import ReportMode
|
from .reportmode import ReportMode
|
||||||
from .temperaturecontrol import TemperatureControl
|
from .temperaturecontrol import TemperatureControl
|
||||||
from .temperaturesensor import TemperatureSensor
|
from .temperaturesensor import TemperatureSensor
|
||||||
@ -64,4 +65,5 @@ __all__ = [
|
|||||||
"FrostProtection",
|
"FrostProtection",
|
||||||
"Thermostat",
|
"Thermostat",
|
||||||
"SmartLightEffect",
|
"SmartLightEffect",
|
||||||
|
"OverheatProtection",
|
||||||
]
|
]
|
||||||
|
@ -10,7 +10,7 @@ class ContactSensor(SmartModule):
|
|||||||
"""Implementation of contact sensor module."""
|
"""Implementation of contact sensor module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = None # we depend on availability of key
|
REQUIRED_COMPONENT = None # we depend on availability of key
|
||||||
REQUIRED_KEY_ON_PARENT = "open"
|
SYSINFO_LOOKUP_KEYS = ["open"]
|
||||||
|
|
||||||
def _initialize_features(self) -> None:
|
def _initialize_features(self) -> None:
|
||||||
"""Initialize features after the initial update."""
|
"""Initialize features after the initial update."""
|
||||||
|
41
kasa/smart/modules/overheatprotection.py
Normal file
41
kasa/smart/modules/overheatprotection.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Overheat module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
|
class OverheatProtection(SmartModule):
|
||||||
|
"""Implementation for overheat_protection."""
|
||||||
|
|
||||||
|
SYSINFO_LOOKUP_KEYS = ["overheated", "overheat_status"]
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
container=self,
|
||||||
|
id="overheated",
|
||||||
|
name="Overheated",
|
||||||
|
attribute_getter="overheated",
|
||||||
|
icon="mdi:heat-wave",
|
||||||
|
type=Feature.Type.BinarySensor,
|
||||||
|
category=Feature.Category.Info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def overheated(self) -> bool:
|
||||||
|
"""Return True if device reports overheating."""
|
||||||
|
if (value := self._device.sys_info.get("overheat_status")) is not None:
|
||||||
|
# Value can be normal, cooldown, or overheated.
|
||||||
|
# We report all but normal as overheated.
|
||||||
|
return value != "normal"
|
||||||
|
|
||||||
|
return self._device.sys_info["overheated"]
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
return {}
|
@ -349,9 +349,8 @@ class SmartDevice(Device):
|
|||||||
) or mod.__name__ in child_modules_to_skip:
|
) or mod.__name__ in child_modules_to_skip:
|
||||||
continue
|
continue
|
||||||
required_component = cast(str, mod.REQUIRED_COMPONENT)
|
required_component = cast(str, mod.REQUIRED_COMPONENT)
|
||||||
if required_component in self._components or (
|
if required_component in self._components or any(
|
||||||
mod.REQUIRED_KEY_ON_PARENT
|
self.sys_info.get(key) is not None for key in mod.SYSINFO_LOOKUP_KEYS
|
||||||
and self.sys_info.get(mod.REQUIRED_KEY_ON_PARENT) is not None
|
|
||||||
):
|
):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Device %s, found required %s, adding %s to modules.",
|
"Device %s, found required %s, adding %s to modules.",
|
||||||
@ -440,19 +439,6 @@ class SmartDevice(Device):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if "overheated" in self._info:
|
|
||||||
self._add_feature(
|
|
||||||
Feature(
|
|
||||||
self,
|
|
||||||
id="overheated",
|
|
||||||
name="Overheated",
|
|
||||||
attribute_getter=lambda x: x._info["overheated"],
|
|
||||||
icon="mdi:heat-wave",
|
|
||||||
type=Feature.Type.BinarySensor,
|
|
||||||
category=Feature.Category.Info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# We check for the key available, and not for the property truthiness,
|
# We check for the key available, and not for the property truthiness,
|
||||||
# as the value is falsy when the device is off.
|
# as the value is falsy when the device is off.
|
||||||
if "on_time" in self._info:
|
if "on_time" in self._info:
|
||||||
|
@ -54,8 +54,8 @@ class SmartModule(Module):
|
|||||||
NAME: str
|
NAME: str
|
||||||
#: Module is initialized, if the given component is available
|
#: Module is initialized, if the given component is available
|
||||||
REQUIRED_COMPONENT: str | None = None
|
REQUIRED_COMPONENT: str | None = None
|
||||||
#: Module is initialized, if the given key available in the main sysinfo
|
#: Module is initialized, if any of the given keys exists in the sysinfo
|
||||||
REQUIRED_KEY_ON_PARENT: str | None = None
|
SYSINFO_LOOKUP_KEYS: list[str] = []
|
||||||
#: Query to execute during the main update cycle
|
#: Query to execute during the main update cycle
|
||||||
QUERY_GETTER_NAME: str
|
QUERY_GETTER_NAME: str
|
||||||
|
|
||||||
|
@ -470,3 +470,61 @@ async def test_smart_temp_range(dev: Device):
|
|||||||
light = dev.modules.get(Module.Light)
|
light = dev.modules.get(Module.Light)
|
||||||
assert light
|
assert light
|
||||||
assert light.valid_temperature_range
|
assert light.valid_temperature_range
|
||||||
|
|
||||||
|
|
||||||
|
@device_smart
|
||||||
|
async def test_initialize_modules_sysinfo_lookup_keys(
|
||||||
|
dev: SmartDevice, mocker: MockerFixture
|
||||||
|
):
|
||||||
|
"""Test that matching modules using SYSINFO_LOOKUP_KEYS are initialized correctly."""
|
||||||
|
|
||||||
|
class AvailableKey(SmartModule):
|
||||||
|
SYSINFO_LOOKUP_KEYS = ["device_id"]
|
||||||
|
|
||||||
|
class NonExistingKey(SmartModule):
|
||||||
|
SYSINFO_LOOKUP_KEYS = ["this_does_not_exist"]
|
||||||
|
|
||||||
|
# The __init_subclass__ hook in smartmodule checks the path,
|
||||||
|
# so we have to manually add these for testing.
|
||||||
|
mocker.patch.dict(
|
||||||
|
"kasa.smart.smartmodule.SmartModule.REGISTERED_MODULES",
|
||||||
|
{
|
||||||
|
AvailableKey._module_name(): AvailableKey,
|
||||||
|
NonExistingKey._module_name(): NonExistingKey,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# We have an already initialized device, so we try to initialize the modules again
|
||||||
|
await dev._initialize_modules()
|
||||||
|
|
||||||
|
assert "AvailableKey" in dev.modules
|
||||||
|
assert "NonExistingKey" not in dev.modules
|
||||||
|
|
||||||
|
|
||||||
|
@device_smart
|
||||||
|
async def test_initialize_modules_required_component(
|
||||||
|
dev: SmartDevice, mocker: MockerFixture
|
||||||
|
):
|
||||||
|
"""Test that matching modules using REQUIRED_COMPONENT are initialized correctly."""
|
||||||
|
|
||||||
|
class AvailableComponent(SmartModule):
|
||||||
|
REQUIRED_COMPONENT = "device"
|
||||||
|
|
||||||
|
class NonExistingComponent(SmartModule):
|
||||||
|
REQUIRED_COMPONENT = "this_does_not_exist"
|
||||||
|
|
||||||
|
# The __init_subclass__ hook in smartmodule checks the path,
|
||||||
|
# so we have to manually add these for testing.
|
||||||
|
mocker.patch.dict(
|
||||||
|
"kasa.smart.smartmodule.SmartModule.REGISTERED_MODULES",
|
||||||
|
{
|
||||||
|
AvailableComponent._module_name(): AvailableComponent,
|
||||||
|
NonExistingComponent._module_name(): NonExistingComponent,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# We have an already initialized device, so we try to initialize the modules again
|
||||||
|
await dev._initialize_modules()
|
||||||
|
|
||||||
|
assert "AvailableComponent" in dev.modules
|
||||||
|
assert "NonExistingComponent" not in dev.modules
|
||||||
|
Loading…
Reference in New Issue
Block a user