mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-04-30 18:46:24 +00:00
Add battery module to smartcam devices (#1452)
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
This commit is contained in:
parent
2542516009
commit
4e7e18cef1
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from .alarm import Alarm
|
from .alarm import Alarm
|
||||||
from .babycrydetection import BabyCryDetection
|
from .babycrydetection import BabyCryDetection
|
||||||
|
from .battery import Battery
|
||||||
from .camera import Camera
|
from .camera import Camera
|
||||||
from .childdevice import ChildDevice
|
from .childdevice import ChildDevice
|
||||||
from .device import DeviceModule
|
from .device import DeviceModule
|
||||||
@ -18,6 +19,7 @@ from .time import Time
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"Alarm",
|
"Alarm",
|
||||||
"BabyCryDetection",
|
"BabyCryDetection",
|
||||||
|
"Battery",
|
||||||
"Camera",
|
"Camera",
|
||||||
"ChildDevice",
|
"ChildDevice",
|
||||||
"DeviceModule",
|
"DeviceModule",
|
||||||
|
113
kasa/smartcam/modules/battery.py
Normal file
113
kasa/smartcam/modules/battery.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
"""Implementation of baby cry detection module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Battery(SmartCamModule):
|
||||||
|
"""Implementation of a battery module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "battery"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
"battery_low",
|
||||||
|
"Battery low",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="battery_low",
|
||||||
|
icon="mdi:alert",
|
||||||
|
type=Feature.Type.BinarySensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
"battery_level",
|
||||||
|
"Battery level",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="battery_percent",
|
||||||
|
icon="mdi:battery",
|
||||||
|
unit_getter=lambda: "%",
|
||||||
|
category=Feature.Category.Info,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
"battery_temperature",
|
||||||
|
"Battery temperature",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="battery_temperature",
|
||||||
|
icon="mdi:battery",
|
||||||
|
unit_getter=lambda: "celsius",
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
"battery_voltage",
|
||||||
|
"Battery voltage",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="battery_voltage",
|
||||||
|
icon="mdi:battery",
|
||||||
|
unit_getter=lambda: "V",
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
"battery_charging",
|
||||||
|
"Battery charging",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="battery_charging",
|
||||||
|
icon="mdi:alert",
|
||||||
|
type=Feature.Type.BinarySensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_percent(self) -> int:
|
||||||
|
"""Return battery level."""
|
||||||
|
return self._device.sys_info["battery_percent"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_low(self) -> bool:
|
||||||
|
"""Return True if battery is low."""
|
||||||
|
return self._device.sys_info["low_battery"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_temperature(self) -> bool:
|
||||||
|
"""Return battery voltage in C."""
|
||||||
|
return self._device.sys_info["battery_temperature"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_voltage(self) -> bool:
|
||||||
|
"""Return battery voltage in V."""
|
||||||
|
return self._device.sys_info["battery_voltage"] / 1_000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_charging(self) -> bool:
|
||||||
|
"""Return True if battery is charging."""
|
||||||
|
return self._device.sys_info["battery_voltage"] != "NO"
|
@ -63,18 +63,14 @@ class SmartCamChild(SmartChildDevice, SmartCamDevice):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _map_child_info_from_parent(self, device_info: dict) -> dict:
|
@staticmethod
|
||||||
return {
|
def _map_child_info_from_parent(device_info: dict) -> dict:
|
||||||
"model": device_info["device_model"],
|
mappings = {
|
||||||
"device_type": device_info["device_type"],
|
"device_model": "model",
|
||||||
"alias": device_info["alias"],
|
"sw_ver": "fw_ver",
|
||||||
"fw_ver": device_info["sw_ver"],
|
"hw_id": "hwId",
|
||||||
"hw_ver": device_info["hw_ver"],
|
|
||||||
"mac": device_info["mac"],
|
|
||||||
"hwId": device_info.get("hw_id"),
|
|
||||||
"oem_id": device_info["oem_id"],
|
|
||||||
"device_id": device_info["device_id"],
|
|
||||||
}
|
}
|
||||||
|
return {mappings.get(k, k): v for k, v in device_info.items()}
|
||||||
|
|
||||||
def _update_internal_state(self, info: dict[str, Any]) -> None:
|
def _update_internal_state(self, info: dict[str, Any]) -> None:
|
||||||
"""Update the internal info state.
|
"""Update the internal info state.
|
||||||
|
@ -238,18 +238,17 @@ class SmartCamDevice(SmartDevice):
|
|||||||
await self._initialize_children()
|
await self._initialize_children()
|
||||||
|
|
||||||
def _map_info(self, device_info: dict) -> dict:
|
def _map_info(self, device_info: dict) -> dict:
|
||||||
|
"""Map the basic keys to the keys used by SmartDevices."""
|
||||||
basic_info = device_info["basic_info"]
|
basic_info = device_info["basic_info"]
|
||||||
return {
|
mappings = {
|
||||||
"model": basic_info["device_model"],
|
"device_model": "model",
|
||||||
"device_type": basic_info["device_type"],
|
"device_alias": "alias",
|
||||||
"alias": basic_info["device_alias"],
|
"sw_version": "fw_ver",
|
||||||
"fw_ver": basic_info["sw_version"],
|
"hw_version": "hw_ver",
|
||||||
"hw_ver": basic_info["hw_version"],
|
"hw_id": "hwId",
|
||||||
"mac": basic_info["mac"],
|
"dev_id": "device_id",
|
||||||
"hwId": basic_info.get("hw_id"),
|
|
||||||
"oem_id": basic_info["oem_id"],
|
|
||||||
"device_id": basic_info["dev_id"],
|
|
||||||
}
|
}
|
||||||
|
return {mappings.get(k, k): v for k, v in basic_info.items()}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
|
@ -33,6 +33,8 @@ class SmartCamModule(SmartModule):
|
|||||||
"BabyCryDetection"
|
"BabyCryDetection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SmartCamBattery: Final[ModuleName[modules.Battery]] = ModuleName("Battery")
|
||||||
|
|
||||||
SmartCamDeviceModule: Final[ModuleName[modules.DeviceModule]] = ModuleName(
|
SmartCamDeviceModule: Final[ModuleName[modules.DeviceModule]] = ModuleName(
|
||||||
"devicemodule"
|
"devicemodule"
|
||||||
)
|
)
|
||||||
|
@ -435,6 +435,15 @@ async def get_device_for_fixture(
|
|||||||
d = device_for_fixture_name(fixture_data.name, fixture_data.protocol)(
|
d = device_for_fixture_name(fixture_data.name, fixture_data.protocol)(
|
||||||
host="127.0.0.123"
|
host="127.0.0.123"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# smart child devices sometimes check _is_hub_child which needs a parent
|
||||||
|
# of DeviceType.Hub
|
||||||
|
class DummyParent:
|
||||||
|
device_type = DeviceType.Hub
|
||||||
|
|
||||||
|
if fixture_data.protocol in {"SMARTCAM.CHILD"}:
|
||||||
|
d._parent = DummyParent()
|
||||||
|
|
||||||
if fixture_data.protocol in {"SMART", "SMART.CHILD"}:
|
if fixture_data.protocol in {"SMART", "SMART.CHILD"}:
|
||||||
d.protocol = FakeSmartProtocol(
|
d.protocol = FakeSmartProtocol(
|
||||||
fixture_data.data, fixture_data.name, verbatim=verbatim
|
fixture_data.data, fixture_data.name, verbatim=verbatim
|
||||||
|
@ -262,7 +262,10 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
child_fixture["get_device_info"]["device_id"] = device_id
|
child_fixture["get_device_info"]["device_id"] = device_id
|
||||||
found_child_fixture_infos.append(child_fixture["get_device_info"])
|
found_child_fixture_infos.append(child_fixture["get_device_info"])
|
||||||
child_protocols[device_id] = FakeSmartProtocol(
|
child_protocols[device_id] = FakeSmartProtocol(
|
||||||
child_fixture, fixture_info_tuple.name, is_child=True
|
child_fixture,
|
||||||
|
fixture_info_tuple.name,
|
||||||
|
is_child=True,
|
||||||
|
verbatim=verbatim,
|
||||||
)
|
)
|
||||||
# Look for fixture inline
|
# Look for fixture inline
|
||||||
elif (child_fixtures := parent_fixture_info.get("child_devices")) and (
|
elif (child_fixtures := parent_fixture_info.get("child_devices")) and (
|
||||||
@ -273,6 +276,7 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
child_fixture,
|
child_fixture,
|
||||||
f"{parent_fixture_name}-{device_id}",
|
f"{parent_fixture_name}-{device_id}",
|
||||||
is_child=True,
|
is_child=True,
|
||||||
|
verbatim=verbatim,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
|
pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
|
||||||
@ -299,7 +303,10 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
# list for smartcam children in order for updates to work.
|
# list for smartcam children in order for updates to work.
|
||||||
found_child_fixture_infos.append(child_fixture[CHILD_INFO_FROM_PARENT])
|
found_child_fixture_infos.append(child_fixture[CHILD_INFO_FROM_PARENT])
|
||||||
child_protocols[device_id] = FakeSmartCamProtocol(
|
child_protocols[device_id] = FakeSmartCamProtocol(
|
||||||
child_fixture, fixture_info_tuple.name, is_child=True
|
child_fixture,
|
||||||
|
fixture_info_tuple.name,
|
||||||
|
is_child=True,
|
||||||
|
verbatim=verbatim,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
warn(
|
warn(
|
||||||
|
@ -6,7 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
from kasa import Credentials, DeviceConfig, SmartProtocol
|
from kasa import Credentials, DeviceConfig, SmartProtocol
|
||||||
from kasa.protocols.smartcamprotocol import SmartCamProtocol
|
from kasa.protocols.smartcamprotocol import SmartCamProtocol
|
||||||
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
|
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT, SmartCamChild
|
||||||
from kasa.transports.basetransport import BaseTransport
|
from kasa.transports.basetransport import BaseTransport
|
||||||
|
|
||||||
from .fakeprotocol_smart import FakeSmartTransport
|
from .fakeprotocol_smart import FakeSmartTransport
|
||||||
@ -243,6 +243,20 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
else:
|
else:
|
||||||
return {"error_code": -1}
|
return {"error_code": -1}
|
||||||
|
|
||||||
|
# smartcam child devices do not make requests for getDeviceInfo as they
|
||||||
|
# get updated from the parent's query. If this is being called from a
|
||||||
|
# child it must be because the fixture has been created directly on the
|
||||||
|
# child device with a dummy parent. In this case return the child info
|
||||||
|
# from parent that's inside the fixture.
|
||||||
|
if (
|
||||||
|
not self.verbatim
|
||||||
|
and method == "getDeviceInfo"
|
||||||
|
and (cifp := info.get(CHILD_INFO_FROM_PARENT))
|
||||||
|
):
|
||||||
|
mapped = SmartCamChild._map_child_info_from_parent(cifp)
|
||||||
|
result = {"device_info": {"basic_info": mapped}}
|
||||||
|
return {"result": result, "error_code": 0}
|
||||||
|
|
||||||
if method in info:
|
if method in info:
|
||||||
params = request_dict.get("params")
|
params = request_dict.get("params")
|
||||||
result = copy.deepcopy(info[method])
|
result = copy.deepcopy(info[method])
|
||||||
|
@ -269,7 +269,7 @@ async def test_hub_children_update_delays(
|
|||||||
for modname, module in child._modules.items():
|
for modname, module in child._modules.items():
|
||||||
if (
|
if (
|
||||||
not (q := module.query())
|
not (q := module.query())
|
||||||
and modname not in {"DeviceModule", "Light"}
|
and modname not in {"DeviceModule", "Light", "Battery", "Camera"}
|
||||||
and not module.SYSINFO_LOOKUP_KEYS
|
and not module.SYSINFO_LOOKUP_KEYS
|
||||||
):
|
):
|
||||||
q = {f"get_dummy_{modname}": {}}
|
q = {f"get_dummy_{modname}": {}}
|
||||||
|
33
tests/smartcam/modules/test_battery.py
Normal file
33
tests/smartcam/modules/test_battery.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""Tests for smartcam battery module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from kasa import Device
|
||||||
|
from kasa.smartcam.smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
from ...device_fixtures import parametrize
|
||||||
|
|
||||||
|
battery_smartcam = parametrize(
|
||||||
|
"has battery",
|
||||||
|
component_filter="battery",
|
||||||
|
protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@battery_smartcam
|
||||||
|
async def test_battery(dev: Device):
|
||||||
|
"""Test device battery."""
|
||||||
|
battery = dev.modules.get(SmartCamModule.SmartCamBattery)
|
||||||
|
assert battery
|
||||||
|
|
||||||
|
feat_ids = {
|
||||||
|
"battery_level",
|
||||||
|
"battery_low",
|
||||||
|
"battery_temperature",
|
||||||
|
"battery_voltage",
|
||||||
|
"battery_charging",
|
||||||
|
}
|
||||||
|
for feat_id in feat_ids:
|
||||||
|
feat = dev.features.get(feat_id)
|
||||||
|
assert feat
|
||||||
|
assert feat.value is not None
|
Loading…
x
Reference in New Issue
Block a user