mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
Move child socket handling to its own SmartStripPlug class (#26)
* All child device handling is moved out from the main smartdevice class, which simplifies the code. * This will also cleanup the constructors as only the subdevices require the ID and the parent reference. * SmartStripPlug offers SmartPlug like interface, but does not allow separate updates * Trying to update() on the children will cause a warning.
This commit is contained in:
parent
489a550582
commit
00276e34b7
@ -2,8 +2,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from pprint import pformat as pf
|
from pprint import pformat as pf
|
||||||
|
import re
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ def dump_discover(ctx, scrub):
|
|||||||
if key in ["latitude_i", "longitude_i"]:
|
if key in ["latitude_i", "longitude_i"]:
|
||||||
val = 0
|
val = 0
|
||||||
else:
|
else:
|
||||||
val = re.sub("\w", "0", val)
|
val = re.sub(r"\w", "0", val)
|
||||||
dev["system"]["get_sysinfo"][key] = val
|
dev["system"]["get_sysinfo"][key] = val
|
||||||
|
|
||||||
model = dev["system"]["get_sysinfo"]["model"]
|
model = dev["system"]["get_sysinfo"]["model"]
|
||||||
|
@ -71,8 +71,8 @@ class SmartBulb(SmartDevice):
|
|||||||
|
|
||||||
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
|
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
|
||||||
|
|
||||||
def __init__(self, host: str, *, child_id: str = None, cache_ttl: int = 3) -> None:
|
def __init__(self, host: str, *, cache_ttl: int = 3) -> None:
|
||||||
SmartDevice.__init__(self, host=host, child_id=child_id, cache_ttl=cache_ttl)
|
SmartDevice.__init__(self, host=host, cache_ttl=cache_ttl)
|
||||||
self.emeter_type = "smartlife.iot.common.emeter"
|
self.emeter_type = "smartlife.iot.common.emeter"
|
||||||
self._device_type = DeviceType.Bulb
|
self._device_type = DeviceType.Bulb
|
||||||
self._light_state = None
|
self._light_state = None
|
||||||
|
@ -11,12 +11,12 @@ Stroetmann which is licensed under the Apache License, Version 2.0.
|
|||||||
You may obtain a copy of the license at
|
You may obtain a copy of the license at
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
"""
|
"""
|
||||||
import functools
|
|
||||||
import inspect
|
|
||||||
import logging
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from kasa.protocol import TPLinkSmartHomeProtocol
|
from kasa.protocol import TPLinkSmartHomeProtocol
|
||||||
@ -103,7 +103,7 @@ def requires_update(f):
|
|||||||
class SmartDevice:
|
class SmartDevice:
|
||||||
"""Base class for all supported device types."""
|
"""Base class for all supported device types."""
|
||||||
|
|
||||||
def __init__(self, host: str, *, child_id: str = None, cache_ttl: int = 3) -> None:
|
def __init__(self, host: str, *, cache_ttl: int = 3) -> None:
|
||||||
"""Create a new SmartDevice instance.
|
"""Create a new SmartDevice instance.
|
||||||
|
|
||||||
:param str host: host name or ip address on which the device listens
|
:param str host: host name or ip address on which the device listens
|
||||||
@ -113,17 +113,11 @@ class SmartDevice:
|
|||||||
|
|
||||||
self.protocol = TPLinkSmartHomeProtocol()
|
self.protocol = TPLinkSmartHomeProtocol()
|
||||||
self.emeter_type = "emeter"
|
self.emeter_type = "emeter"
|
||||||
self.child_id = child_id
|
|
||||||
self.cache_ttl = timedelta(seconds=cache_ttl)
|
self.cache_ttl = timedelta(seconds=cache_ttl)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("Initializing %s with cache ttl %s", self.host, self.cache_ttl)
|
||||||
"Initializing %s using child_id %s and cache ttl %s",
|
|
||||||
self.host,
|
|
||||||
self.child_id,
|
|
||||||
self.cache_ttl,
|
|
||||||
)
|
|
||||||
self.cache = defaultdict(lambda: defaultdict(lambda: None)) # type: ignore
|
self.cache = defaultdict(lambda: defaultdict(lambda: None)) # type: ignore
|
||||||
self._device_type = DeviceType.Unknown
|
self._device_type = DeviceType.Unknown
|
||||||
self._sys_info = None
|
self._sys_info: Optional[Dict] = None
|
||||||
|
|
||||||
def _result_from_cache(self, target, cmd) -> Optional[Dict]:
|
def _result_from_cache(self, target, cmd) -> Optional[Dict]:
|
||||||
"""Return query result from cache if still fresh.
|
"""Return query result from cache if still fresh.
|
||||||
@ -162,7 +156,7 @@ class SmartDevice:
|
|||||||
self.cache[target][cmd]["last_updated"] = datetime.utcnow()
|
self.cache[target][cmd]["last_updated"] = datetime.utcnow()
|
||||||
|
|
||||||
async def _query_helper(
|
async def _query_helper(
|
||||||
self, target: str, cmd: str, arg: Optional[Dict] = None
|
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Handle result unwrapping and error handling.
|
"""Handle result unwrapping and error handling.
|
||||||
|
|
||||||
@ -174,8 +168,8 @@ class SmartDevice:
|
|||||||
:raises SmartDeviceException: if command was not executed correctly
|
:raises SmartDeviceException: if command was not executed correctly
|
||||||
"""
|
"""
|
||||||
request: Dict[str, Any] = {target: {cmd: arg}}
|
request: Dict[str, Any] = {target: {cmd: arg}}
|
||||||
if self.child_id is not None:
|
if child_ids is not None:
|
||||||
request = {"context": {"child_ids": [self.child_id]}, target: {cmd: arg}}
|
request = {"context": {"child_ids": child_ids}, target: {cmd: arg}}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._result_from_cache(target, cmd)
|
response = self._result_from_cache(target, cmd)
|
||||||
@ -204,16 +198,6 @@ class SmartDevice:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _get_child_info(self) -> Dict:
|
|
||||||
"""Return the child information dict, if available.
|
|
||||||
|
|
||||||
:raises SmartDeviceException: if there is no child or it cannot be found.
|
|
||||||
"""
|
|
||||||
for plug in self.sys_info["children"]:
|
|
||||||
if plug["id"] == self.child_id:
|
|
||||||
return plug
|
|
||||||
raise SmartDeviceException("Unable to find children %s")
|
|
||||||
|
|
||||||
def has_emeter(self) -> bool:
|
def has_emeter(self) -> bool:
|
||||||
"""Return if device has an energy meter.
|
"""Return if device has an energy meter.
|
||||||
|
|
||||||
@ -614,11 +598,8 @@ class SmartDevice:
|
|||||||
def device_id(self) -> str:
|
def device_id(self) -> str:
|
||||||
"""Return unique ID for the device.
|
"""Return unique ID for the device.
|
||||||
|
|
||||||
For regular devices this is the MAC address of the device,
|
This is the MAC address of the device.
|
||||||
for child devices a combination of MAC and child's ID.
|
|
||||||
"""
|
"""
|
||||||
if self.is_child_device:
|
|
||||||
return f"{self.mac}_{self.child_id}"
|
|
||||||
return self.mac
|
return self.mac
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -651,11 +632,6 @@ class SmartDevice:
|
|||||||
"""Return True if the device supports color temperature."""
|
"""Return True if the device supports color temperature."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
|
||||||
def is_child_device(self) -> bool:
|
|
||||||
"""Return True if the device is a child device of another device."""
|
|
||||||
return self.child_id is not None
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{} model {} at {} ({}), is_on: {} - dev specific: {}>".format(
|
return "<{} model {} at {} ({}), is_on: {} - dev specific: {}>".format(
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
|
@ -35,8 +35,8 @@ class SmartPlug(SmartDevice):
|
|||||||
and should be handled by the user of the library.
|
and should be handled by the user of the library.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host: str, *, child_id: str = None, cache_ttl: int = 3) -> None:
|
def __init__(self, host: str, *, cache_ttl: int = 3) -> None:
|
||||||
SmartDevice.__init__(self, host, child_id=child_id, cache_ttl=cache_ttl)
|
SmartDevice.__init__(self, host, cache_ttl=cache_ttl)
|
||||||
self.emeter_type = "emeter"
|
self.emeter_type = "emeter"
|
||||||
self._device_type = DeviceType.Plug
|
self._device_type = DeviceType.Plug
|
||||||
|
|
||||||
@ -81,20 +81,6 @@ class SmartPlug(SmartDevice):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Brightness value %s is not valid." % value)
|
raise ValueError("Brightness value %s is not valid." % value)
|
||||||
|
|
||||||
@property # type: ignore
|
|
||||||
@requires_update
|
|
||||||
def alias(self) -> str:
|
|
||||||
"""Return device name (alias).
|
|
||||||
|
|
||||||
:return: Device name aka alias.
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
if self.is_child_device:
|
|
||||||
info = self._get_child_info()
|
|
||||||
return info["alias"]
|
|
||||||
else:
|
|
||||||
return super().alias
|
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def is_dimmable(self):
|
def is_dimmable(self):
|
||||||
@ -125,10 +111,6 @@ class SmartPlug(SmartDevice):
|
|||||||
|
|
||||||
:return: True if device is on, False otherwise
|
:return: True if device is on, False otherwise
|
||||||
"""
|
"""
|
||||||
if self.is_child_device:
|
|
||||||
info = self._get_child_info()
|
|
||||||
return info["state"]
|
|
||||||
|
|
||||||
sys_info = self.sys_info
|
sys_info = self.sys_info
|
||||||
return bool(sys_info["relay_state"])
|
return bool(sys_info["relay_state"])
|
||||||
|
|
||||||
@ -176,12 +158,7 @@ class SmartPlug(SmartDevice):
|
|||||||
:return: datetime for on since
|
:return: datetime for on since
|
||||||
:rtype: datetime
|
:rtype: datetime
|
||||||
"""
|
"""
|
||||||
sys_info = self.sys_info
|
on_time = self.sys_info["on_time"]
|
||||||
if self.is_child_device:
|
|
||||||
info = self._get_child_info()
|
|
||||||
on_time = info["on_time"]
|
|
||||||
else:
|
|
||||||
on_time = sys_info["on_time"]
|
|
||||||
|
|
||||||
return datetime.datetime.now() - datetime.timedelta(seconds=on_time)
|
return datetime.datetime.now() - datetime.timedelta(seconds=on_time)
|
||||||
|
|
||||||
|
@ -2,18 +2,23 @@
|
|||||||
|
|
||||||
.. todo:: describe how this interfaces with single plugs.
|
.. todo:: describe how this interfaces with single plugs.
|
||||||
"""
|
"""
|
||||||
|
from collections import defaultdict
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from typing import Any, DefaultDict, Dict, List, Optional
|
||||||
from typing import Any, DefaultDict, Dict, List
|
|
||||||
|
|
||||||
from kasa.smartdevice import DeviceType, requires_update
|
from kasa.smartdevice import (
|
||||||
|
DeviceType,
|
||||||
|
SmartDevice,
|
||||||
|
SmartDeviceException,
|
||||||
|
requires_update,
|
||||||
|
)
|
||||||
from kasa.smartplug import SmartPlug
|
from kasa.smartplug import SmartPlug
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SmartStrip(SmartPlug):
|
class SmartStrip(SmartDevice):
|
||||||
"""Representation of a TP-Link Smart Power Strip.
|
"""Representation of a TP-Link Smart Power Strip.
|
||||||
|
|
||||||
Usage example when used as library:
|
Usage example when used as library:
|
||||||
@ -40,11 +45,15 @@ class SmartStrip(SmartPlug):
|
|||||||
and should be handled by the user of the library.
|
and should be handled by the user of the library.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def has_emeter(self) -> bool:
|
||||||
|
"""Return True as strips has always an emeter."""
|
||||||
|
return True
|
||||||
|
|
||||||
def __init__(self, host: str, *, cache_ttl: int = 3) -> None:
|
def __init__(self, host: str, *, cache_ttl: int = 3) -> None:
|
||||||
SmartPlug.__init__(self, host=host, cache_ttl=cache_ttl)
|
SmartDevice.__init__(self, host=host, cache_ttl=cache_ttl)
|
||||||
self.emeter_type = "emeter"
|
self.emeter_type = "emeter"
|
||||||
self._device_type = DeviceType.Strip
|
self._device_type = DeviceType.Strip
|
||||||
self.plugs: List[SmartPlug] = []
|
self.plugs: List[SmartStripPlug] = []
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -66,11 +75,12 @@ class SmartStrip(SmartPlug):
|
|||||||
# Initialize the child devices during the first update.
|
# Initialize the child devices during the first update.
|
||||||
if not self.plugs:
|
if not self.plugs:
|
||||||
children = self.sys_info["children"]
|
children = self.sys_info["children"]
|
||||||
self.num_children = len(children)
|
_LOGGER.debug("Initializing %s child sockets", len(children))
|
||||||
for child in children:
|
for child in children:
|
||||||
self.plugs.append(
|
self.plugs.append(
|
||||||
SmartPlug(
|
SmartStripPlug(
|
||||||
self.host,
|
self.host,
|
||||||
|
parent=self,
|
||||||
child_id=child["id"],
|
child_id=child["id"],
|
||||||
cache_ttl=self.cache_ttl.total_seconds(),
|
cache_ttl=self.cache_ttl.total_seconds(),
|
||||||
)
|
)
|
||||||
@ -98,6 +108,30 @@ class SmartStrip(SmartPlug):
|
|||||||
"""Return the maximum on-time of all outlets."""
|
"""Return the maximum on-time of all outlets."""
|
||||||
return max(plug.on_since for plug in self.plugs)
|
return max(plug.on_since for plug in self.plugs)
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def led(self) -> bool:
|
||||||
|
"""Return the state of the led.
|
||||||
|
|
||||||
|
:return: True if led is on, False otherwise
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
# TODO this is a copypaste from smartplug,
|
||||||
|
# check if led value is per socket or per device..
|
||||||
|
sys_info = self.sys_info
|
||||||
|
return bool(1 - sys_info["led_off"])
|
||||||
|
|
||||||
|
async def set_led(self, state: bool):
|
||||||
|
"""Set the state of the led (night mode).
|
||||||
|
|
||||||
|
:param bool state: True to set led on, False to set led off
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
"""
|
||||||
|
# TODO this is a copypaste from smartplug,
|
||||||
|
# check if led value is per socket or per device..
|
||||||
|
await self._query_helper("system", "set_led_off", {"off": int(not state)})
|
||||||
|
await self.update()
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def state_information(self) -> Dict[str, Any]:
|
def state_information(self) -> Dict[str, Any]:
|
||||||
@ -188,3 +222,120 @@ class SmartStrip(SmartPlug):
|
|||||||
"""
|
"""
|
||||||
for plug in self.plugs:
|
for plug in self.plugs:
|
||||||
await plug.erase_emeter_stats()
|
await plug.erase_emeter_stats()
|
||||||
|
|
||||||
|
|
||||||
|
class SmartStripPlug(SmartPlug):
|
||||||
|
"""Representation of a single socket in a power strip.
|
||||||
|
|
||||||
|
This allows you to use the sockets as they were SmartPlug objects.
|
||||||
|
Instead of calling an update on any of these, you should call an update
|
||||||
|
on the parent device before accessing the properties.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, host: str, parent: "SmartStrip", child_id: str, *, cache_ttl: int = 3
|
||||||
|
) -> None:
|
||||||
|
super().__init__(host, cache_ttl=cache_ttl)
|
||||||
|
|
||||||
|
self.parent = parent
|
||||||
|
self.child_id = child_id
|
||||||
|
self._sys_info = self._get_child_info()
|
||||||
|
|
||||||
|
async def update(self):
|
||||||
|
"""Override the update to no-op and inform the user."""
|
||||||
|
_LOGGER.warning(
|
||||||
|
"You called update() on a child device, which has no effect."
|
||||||
|
"Call update() on the parent device instead."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _query_helper(
|
||||||
|
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
|
||||||
|
) -> Any:
|
||||||
|
"""Override query helper to include the child_ids."""
|
||||||
|
return await self.parent._query_helper(
|
||||||
|
target, cmd, arg, child_ids=[self.child_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return whether device is on.
|
||||||
|
|
||||||
|
:return: True if device is on, False otherwise
|
||||||
|
"""
|
||||||
|
info = self._get_child_info()
|
||||||
|
return info["state"]
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def led(self) -> bool:
|
||||||
|
"""Return the state of the led.
|
||||||
|
|
||||||
|
This is always false for subdevices.
|
||||||
|
|
||||||
|
:return: True if led is on, False otherwise
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def device_id(self) -> str:
|
||||||
|
"""Return unique ID for the socket.
|
||||||
|
|
||||||
|
This is a combination of MAC and child's ID.
|
||||||
|
"""
|
||||||
|
return f"{self.mac}_{self.child_id}"
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def has_emeter(self):
|
||||||
|
"""Single sockets have always an emeter."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def alias(self) -> str:
|
||||||
|
"""Return device name (alias).
|
||||||
|
|
||||||
|
:return: Device name aka alias.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
info = self._get_child_info()
|
||||||
|
return info["alias"]
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def on_since(self) -> datetime.datetime:
|
||||||
|
"""Return pretty-printed on-time.
|
||||||
|
|
||||||
|
:return: datetime for on since
|
||||||
|
:rtype: datetime
|
||||||
|
"""
|
||||||
|
info = self._get_child_info()
|
||||||
|
on_time = info["on_time"]
|
||||||
|
|
||||||
|
return datetime.datetime.now() - datetime.timedelta(seconds=on_time)
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
@requires_update
|
||||||
|
def model(self) -> str:
|
||||||
|
"""Return device model for a child socket.
|
||||||
|
|
||||||
|
:return: device model
|
||||||
|
:rtype: str
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
"""
|
||||||
|
sys_info = self.parent.sys_info
|
||||||
|
return f"Socket for {sys_info['model']}"
|
||||||
|
|
||||||
|
def _get_child_info(self) -> Dict:
|
||||||
|
"""Return the subdevice information for this device.
|
||||||
|
|
||||||
|
:raises SmartDeviceException: if the information is not found.
|
||||||
|
"""
|
||||||
|
for plug in self.parent.sys_info["children"]:
|
||||||
|
if plug["id"] == self.child_id:
|
||||||
|
return plug
|
||||||
|
raise SmartDeviceException(f"Unable to find children {self.child_id}")
|
||||||
|
@ -52,7 +52,7 @@ dimmable = pytest.mark.parametrize(
|
|||||||
"dev", filter_model("dimmable", DIMMABLE), indirect=True
|
"dev", filter_model("dimmable", DIMMABLE), indirect=True
|
||||||
)
|
)
|
||||||
non_dimmable = pytest.mark.parametrize(
|
non_dimmable = pytest.mark.parametrize(
|
||||||
"dev", filter_model("non-dimmable", ALL_DEVICES - DIMMABLE), indirect=True
|
"dev", filter_model("non-dimmable", ALL_DEVICES - DIMMABLE - STRIPS), indirect=True
|
||||||
)
|
)
|
||||||
|
|
||||||
variable_temp = pytest.mark.parametrize(
|
variable_temp = pytest.mark.parametrize(
|
||||||
|
Loading…
Reference in New Issue
Block a user