mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-04-25 16:16:22 +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 .babycrydetection import BabyCryDetection
|
||||
from .battery import Battery
|
||||
from .camera import Camera
|
||||
from .childdevice import ChildDevice
|
||||
from .device import DeviceModule
|
||||
@ -18,6 +19,7 @@ from .time import Time
|
||||
__all__ = [
|
||||
"Alarm",
|
||||
"BabyCryDetection",
|
||||
"Battery",
|
||||
"Camera",
|
||||
"ChildDevice",
|
||||
"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,
|
||||
)
|
||||
|
||||
def _map_child_info_from_parent(self, device_info: dict) -> dict:
|
||||
return {
|
||||
"model": device_info["device_model"],
|
||||
"device_type": device_info["device_type"],
|
||||
"alias": device_info["alias"],
|
||||
"fw_ver": device_info["sw_ver"],
|
||||
"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"],
|
||||
@staticmethod
|
||||
def _map_child_info_from_parent(device_info: dict) -> dict:
|
||||
mappings = {
|
||||
"device_model": "model",
|
||||
"sw_ver": "fw_ver",
|
||||
"hw_id": "hwId",
|
||||
}
|
||||
return {mappings.get(k, k): v for k, v in device_info.items()}
|
||||
|
||||
def _update_internal_state(self, info: dict[str, Any]) -> None:
|
||||
"""Update the internal info state.
|
||||
|
@ -238,18 +238,17 @@ class SmartCamDevice(SmartDevice):
|
||||
await self._initialize_children()
|
||||
|
||||
def _map_info(self, device_info: dict) -> dict:
|
||||
"""Map the basic keys to the keys used by SmartDevices."""
|
||||
basic_info = device_info["basic_info"]
|
||||
return {
|
||||
"model": basic_info["device_model"],
|
||||
"device_type": basic_info["device_type"],
|
||||
"alias": basic_info["device_alias"],
|
||||
"fw_ver": basic_info["sw_version"],
|
||||
"hw_ver": basic_info["hw_version"],
|
||||
"mac": basic_info["mac"],
|
||||
"hwId": basic_info.get("hw_id"),
|
||||
"oem_id": basic_info["oem_id"],
|
||||
"device_id": basic_info["dev_id"],
|
||||
mappings = {
|
||||
"device_model": "model",
|
||||
"device_alias": "alias",
|
||||
"sw_version": "fw_ver",
|
||||
"hw_version": "hw_ver",
|
||||
"hw_id": "hwId",
|
||||
"dev_id": "device_id",
|
||||
}
|
||||
return {mappings.get(k, k): v for k, v in basic_info.items()}
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
@ -33,6 +33,8 @@ class SmartCamModule(SmartModule):
|
||||
"BabyCryDetection"
|
||||
)
|
||||
|
||||
SmartCamBattery: Final[ModuleName[modules.Battery]] = ModuleName("Battery")
|
||||
|
||||
SmartCamDeviceModule: Final[ModuleName[modules.DeviceModule]] = ModuleName(
|
||||
"devicemodule"
|
||||
)
|
||||
|
@ -435,6 +435,15 @@ async def get_device_for_fixture(
|
||||
d = device_for_fixture_name(fixture_data.name, fixture_data.protocol)(
|
||||
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"}:
|
||||
d.protocol = FakeSmartProtocol(
|
||||
fixture_data.data, fixture_data.name, verbatim=verbatim
|
||||
|
@ -262,7 +262,10 @@ class FakeSmartTransport(BaseTransport):
|
||||
child_fixture["get_device_info"]["device_id"] = device_id
|
||||
found_child_fixture_infos.append(child_fixture["get_device_info"])
|
||||
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
|
||||
elif (child_fixtures := parent_fixture_info.get("child_devices")) and (
|
||||
@ -273,6 +276,7 @@ class FakeSmartTransport(BaseTransport):
|
||||
child_fixture,
|
||||
f"{parent_fixture_name}-{device_id}",
|
||||
is_child=True,
|
||||
verbatim=verbatim,
|
||||
)
|
||||
else:
|
||||
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.
|
||||
found_child_fixture_infos.append(child_fixture[CHILD_INFO_FROM_PARENT])
|
||||
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:
|
||||
warn(
|
||||
|
@ -6,7 +6,7 @@ from typing import Any
|
||||
|
||||
from kasa import Credentials, DeviceConfig, SmartProtocol
|
||||
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 .fakeprotocol_smart import FakeSmartTransport
|
||||
@ -243,6 +243,20 @@ class FakeSmartCamTransport(BaseTransport):
|
||||
else:
|
||||
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:
|
||||
params = request_dict.get("params")
|
||||
result = copy.deepcopy(info[method])
|
||||
|
@ -269,7 +269,7 @@ async def test_hub_children_update_delays(
|
||||
for modname, module in child._modules.items():
|
||||
if (
|
||||
not (q := module.query())
|
||||
and modname not in {"DeviceModule", "Light"}
|
||||
and modname not in {"DeviceModule", "Light", "Battery", "Camera"}
|
||||
and not module.SYSINFO_LOOKUP_KEYS
|
||||
):
|
||||
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