mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-04-26 16:46:23 +00:00
Add support for HS300 power strip (#137)
* discover runs, prints on since of device 0 * added preliminary support for HS300 * forgot to add smartdevice to commit * added index to CLI * clean up dirty code * added fake sysinfo_hs300 * changed device alias to match MAC * #131 Move _id_to_index into smartstrip so everyone can pass index * Update pyHS100/discover.py Co-Authored-By: jimboca <jimboca3@gmail.com> * refactoring to deduplicate code between smarplug and smartstrip * fixing CI failures for devices without children * incorporating feedback from pull request. * fixing hound violation * changed internal store from list of dicts to dict * changed other methods to dictionary store as well * removed unused optional type from imports * changed plugs to Dict, remove redundant sys_info calls * added more functionality for smart strip, added smart strip tests * updated FakeTransportProtocol for devices with children * corrected hound violations * add click-datetime
This commit is contained in:
parent
ae53e8de1e
commit
6115d96c39
@ -14,6 +14,8 @@ Python Library to control TPLink smart plugs/switches and smart bulbs.
|
|||||||
* HS103
|
* HS103
|
||||||
* HS105
|
* HS105
|
||||||
* HS110
|
* HS110
|
||||||
|
* Power Strips
|
||||||
|
* HS300
|
||||||
* Wall switches
|
* Wall switches
|
||||||
* HS200
|
* HS200
|
||||||
* HS210
|
* HS210
|
||||||
|
3
pyHS100/__init__.py
Normal file → Executable file
3
pyHS100/__init__.py
Normal file → Executable file
@ -13,8 +13,9 @@ Module-specific errors are raised as `SmartDeviceException` and are expected
|
|||||||
to be handled by the user of the library.
|
to be handled by the user of the library.
|
||||||
"""
|
"""
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
from .smartdevice import SmartDevice, SmartDeviceException
|
from .smartdevice import SmartDevice, SmartDeviceException, EmeterStatus
|
||||||
from .smartplug import SmartPlug
|
from .smartplug import SmartPlug
|
||||||
from .smartbulb import SmartBulb
|
from .smartbulb import SmartBulb
|
||||||
|
from .smartstrip import SmartStrip, SmartStripException
|
||||||
from .protocol import TPLinkSmartHomeProtocol
|
from .protocol import TPLinkSmartHomeProtocol
|
||||||
from .discover import Discover
|
from .discover import Discover
|
||||||
|
44
pyHS100/cli.py
Normal file → Executable file
44
pyHS100/cli.py
Normal file → Executable file
@ -12,6 +12,7 @@ if sys.version_info < (3, 4):
|
|||||||
from pyHS100 import (SmartDevice,
|
from pyHS100 import (SmartDevice,
|
||||||
SmartPlug,
|
SmartPlug,
|
||||||
SmartBulb,
|
SmartBulb,
|
||||||
|
SmartStrip,
|
||||||
Discover) # noqa: E402
|
Discover) # noqa: E402
|
||||||
|
|
||||||
pass_dev = click.make_pass_decorator(SmartDevice)
|
pass_dev = click.make_pass_decorator(SmartDevice)
|
||||||
@ -29,8 +30,9 @@ pass_dev = click.make_pass_decorator(SmartDevice)
|
|||||||
@click.option('--debug/--normal', default=False)
|
@click.option('--debug/--normal', default=False)
|
||||||
@click.option('--bulb', default=False, is_flag=True)
|
@click.option('--bulb', default=False, is_flag=True)
|
||||||
@click.option('--plug', default=False, is_flag=True)
|
@click.option('--plug', default=False, is_flag=True)
|
||||||
|
@click.option('--strip', default=False, is_flag=True)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, ip, host, alias, debug, bulb, plug):
|
def cli(ctx, ip, host, alias, debug, bulb, plug, strip):
|
||||||
"""A cli tool for controlling TP-Link smart home plugs."""
|
"""A cli tool for controlling TP-Link smart home plugs."""
|
||||||
if debug:
|
if debug:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
@ -58,15 +60,18 @@ def cli(ctx, ip, host, alias, debug, bulb, plug):
|
|||||||
ctx.invoke(discover)
|
ctx.invoke(discover)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if not bulb and not plug:
|
if not bulb and not plug and not strip:
|
||||||
click.echo("No --bulb nor --plug given, discovering..")
|
click.echo("No --strip nor --bulb nor --plug given, discovering..")
|
||||||
dev = Discover.discover_single(host)
|
dev = Discover.discover_single(host)
|
||||||
elif bulb:
|
elif bulb:
|
||||||
dev = SmartBulb(host)
|
dev = SmartBulb(host)
|
||||||
elif plug:
|
elif plug:
|
||||||
dev = SmartPlug(host)
|
dev = SmartPlug(host)
|
||||||
|
elif strip:
|
||||||
|
dev = SmartStrip(host)
|
||||||
else:
|
else:
|
||||||
click.echo("Unable to detect type, use --bulb or --plug!")
|
click.echo(
|
||||||
|
"Unable to detect type, use --strip or --bulb or --plug!")
|
||||||
return
|
return
|
||||||
ctx.obj = dev
|
ctx.obj = dev
|
||||||
|
|
||||||
@ -168,13 +173,22 @@ def emeter(dev, year, month, erase):
|
|||||||
dev.erase_emeter_stats()
|
dev.erase_emeter_stats()
|
||||||
return
|
return
|
||||||
|
|
||||||
click.echo("Current state: %s" % dev.get_emeter_realtime())
|
|
||||||
if year:
|
if year:
|
||||||
click.echo("== For year %s ==" % year.year)
|
click.echo("== For year %s ==" % year.year)
|
||||||
click.echo(dev.get_emeter_monthly(year.year))
|
emeter_status = dev.get_emeter_monthly(year.year)
|
||||||
elif month:
|
elif month:
|
||||||
click.echo("== For month %s of %s ==" % (month.month, month.year))
|
click.echo("== For month %s of %s ==" % (month.month, month.year))
|
||||||
dev.get_emeter_daily(year=month.year, month=month.month)
|
emeter_status = dev.get_emeter_daily(year=month.year,
|
||||||
|
month=month.month)
|
||||||
|
else:
|
||||||
|
emeter_status = dev.get_emeter_realtime()
|
||||||
|
click.echo("== Current State ==")
|
||||||
|
|
||||||
|
if isinstance(emeter_status, list):
|
||||||
|
for plug in emeter_status:
|
||||||
|
click.echo("Plug %d: %s" % (emeter_status.index(plug) + 1, plug))
|
||||||
|
else:
|
||||||
|
click.echo("%s" % emeter_status)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@ -245,19 +259,27 @@ def time(dev):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@click.argument('index', type=int, required=False)
|
||||||
@pass_dev
|
@pass_dev
|
||||||
def on(plug):
|
def on(plug, index):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
click.echo("Turning on..")
|
click.echo("Turning on..")
|
||||||
plug.turn_on()
|
if index is None:
|
||||||
|
plug.turn_on()
|
||||||
|
else:
|
||||||
|
plug.turn_on(index=(index - 1))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@click.argument('index', type=int, required=False)
|
||||||
@pass_dev
|
@pass_dev
|
||||||
def off(plug):
|
def off(plug, index):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
click.echo("Turning off..")
|
click.echo("Turning off..")
|
||||||
plug.turn_off()
|
if index is None:
|
||||||
|
plug.turn_off()
|
||||||
|
else:
|
||||||
|
plug.turn_off(index=(index - 1))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
8
pyHS100/discover.py
Normal file → Executable file
8
pyHS100/discover.py
Normal file → Executable file
@ -3,7 +3,8 @@ import logging
|
|||||||
import json
|
import json
|
||||||
from typing import Dict, Type
|
from typing import Dict, Type
|
||||||
|
|
||||||
from pyHS100 import TPLinkSmartHomeProtocol, SmartDevice, SmartPlug, SmartBulb
|
from pyHS100 import (TPLinkSmartHomeProtocol, SmartDevice, SmartPlug,
|
||||||
|
SmartBulb, SmartStrip)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -98,7 +99,10 @@ class Discover:
|
|||||||
type = "UNKNOWN"
|
type = "UNKNOWN"
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("No 'system' nor 'get_sysinfo' in response")
|
_LOGGER.error("No 'system' nor 'get_sysinfo' in response")
|
||||||
if "smartplug" in type.lower():
|
|
||||||
|
if "smartplug" in type.lower() and "children" in sysinfo:
|
||||||
|
return SmartStrip
|
||||||
|
elif "smartplug" in type.lower():
|
||||||
return SmartPlug
|
return SmartPlug
|
||||||
elif "smartbulb" in type.lower():
|
elif "smartbulb" in type.lower():
|
||||||
return SmartBulb
|
return SmartBulb
|
||||||
|
0
pyHS100/protocol.py
Normal file → Executable file
0
pyHS100/protocol.py
Normal file → Executable file
24
pyHS100/smartdevice.py
Normal file → Executable file
24
pyHS100/smartdevice.py
Normal file → Executable file
@ -74,17 +74,20 @@ class SmartDevice(object):
|
|||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
host: str,
|
host: str,
|
||||||
protocol: Optional[TPLinkSmartHomeProtocol] = None) -> None:
|
protocol: Optional[TPLinkSmartHomeProtocol] = None,
|
||||||
|
context: str = None) -> 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
|
||||||
|
:param context: optional child ID for context in a parent device
|
||||||
"""
|
"""
|
||||||
self.host = host
|
self.host = host
|
||||||
if not protocol:
|
if not protocol:
|
||||||
protocol = TPLinkSmartHomeProtocol()
|
protocol = TPLinkSmartHomeProtocol()
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.emeter_type = "emeter" # type: str
|
self.emeter_type = "emeter" # type: str
|
||||||
|
self.context = context
|
||||||
|
|
||||||
def _query_helper(self,
|
def _query_helper(self,
|
||||||
target: str,
|
target: str,
|
||||||
@ -100,12 +103,17 @@ class SmartDevice(object):
|
|||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises SmartDeviceException: if command was not executed correctly
|
:raises SmartDeviceException: if command was not executed correctly
|
||||||
"""
|
"""
|
||||||
|
if self.context is None:
|
||||||
|
request = {target: {cmd: arg}}
|
||||||
|
else:
|
||||||
|
request = {"context": {"child_ids": [self.context]},
|
||||||
|
target: {cmd: arg}}
|
||||||
if arg is None:
|
if arg is None:
|
||||||
arg = {}
|
arg = {}
|
||||||
try:
|
try:
|
||||||
response = self.protocol.query(
|
response = self.protocol.query(
|
||||||
host=self.host,
|
host=self.host,
|
||||||
request={target: {cmd: arg}}
|
request=request,
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise SmartDeviceException('Communication error') from ex
|
raise SmartDeviceException('Communication error') from ex
|
||||||
@ -384,11 +392,11 @@ class SmartDevice(object):
|
|||||||
|
|
||||||
def get_emeter_realtime(self) -> Optional[Dict]:
|
def get_emeter_realtime(self) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
Retrive current energy readings from device.
|
Retrieve current energy readings from device.
|
||||||
|
|
||||||
:returns: current readings or False
|
:returns: current readings or False
|
||||||
:rtype: dict, None
|
:rtype: dict, None
|
||||||
None if device has no energy meter or error occured
|
None if device has no energy meter or error occurred
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter:
|
if not self.has_emeter:
|
||||||
@ -405,11 +413,11 @@ class SmartDevice(object):
|
|||||||
Retrieve daily statistics for a given month
|
Retrieve daily statistics for a given month
|
||||||
|
|
||||||
:param year: year for which to retrieve statistics (default: this year)
|
:param year: year for which to retrieve statistics (default: this year)
|
||||||
:param month: month for which to retrieve statistcs (default: this
|
:param month: month for which to retrieve statistics (default: this
|
||||||
month)
|
month)
|
||||||
:param kwh: return usage in kWh (default: True)
|
:param kwh: return usage in kWh (default: True)
|
||||||
:return: mapping of day of month to value
|
:return: mapping of day of month to value
|
||||||
None if device has no energy meter or error occured
|
None if device has no energy meter or error occurred
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
@ -483,9 +491,9 @@ class SmartDevice(object):
|
|||||||
|
|
||||||
def current_consumption(self) -> Optional[float]:
|
def current_consumption(self) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Get the current power consumption in Watt.
|
Get the current power consumption in Watts.
|
||||||
|
|
||||||
:return: the current power consumption in Watt.
|
:return: the current power consumption in Watts.
|
||||||
None if device has no energy meter.
|
None if device has no energy meter.
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
|
@ -33,9 +33,10 @@ class SmartPlug(SmartDevice):
|
|||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
host: str,
|
host: str,
|
||||||
protocol: 'TPLinkSmartHomeProtocol' = None) -> None:
|
protocol: 'TPLinkSmartHomeProtocol' = None,
|
||||||
SmartDevice.__init__(self, host, protocol)
|
context: str = None) -> None:
|
||||||
self.emeter_type = "emeter"
|
SmartDevice.__init__(self, host, protocol, context)
|
||||||
|
self._type = "emeter"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def state(self) -> str:
|
||||||
@ -126,7 +127,6 @@ class SmartPlug(SmartDevice):
|
|||||||
|
|
||||||
:return: True if switch supports brightness changes, False otherwise
|
:return: True if switch supports brightness changes, False otherwise
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return "brightness" in self.sys_info
|
return "brightness" in self.sys_info
|
||||||
|
|
||||||
@ -193,8 +193,15 @@ class SmartPlug(SmartDevice):
|
|||||||
:return: datetime for on since
|
:return: datetime for on since
|
||||||
:rtype: datetime
|
:rtype: datetime
|
||||||
"""
|
"""
|
||||||
return datetime.datetime.now() - \
|
if self.context:
|
||||||
datetime.timedelta(seconds=self.sys_info["on_time"])
|
for plug in self.sys_info["children"]:
|
||||||
|
if plug["id"] == self.context:
|
||||||
|
on_time = plug["on_time"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
on_time = self.sys_info["on_time"]
|
||||||
|
|
||||||
|
return datetime.datetime.now() - datetime.timedelta(seconds=on_time)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_information(self) -> Dict[str, Any]:
|
def state_information(self) -> Dict[str, Any]:
|
||||||
|
397
pyHS100/smartstrip.py
Executable file
397
pyHS100/smartstrip.py
Executable file
@ -0,0 +1,397 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
|
from pyHS100 import SmartPlug, SmartDeviceException, EmeterStatus
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SmartStripException(SmartDeviceException):
|
||||||
|
"""
|
||||||
|
SmartStripException gets raised for errors specific to the smart strip.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SmartStrip(SmartPlug):
|
||||||
|
"""Representation of a TP-Link Smart Power Strip.
|
||||||
|
|
||||||
|
Usage example when used as library:
|
||||||
|
p = SmartStrip("192.168.1.105")
|
||||||
|
# print the devices alias
|
||||||
|
print(p.alias)
|
||||||
|
# change state of plug
|
||||||
|
p.state = "ON"
|
||||||
|
p.state = "OFF"
|
||||||
|
# query and print current state of plug
|
||||||
|
print(p.state)
|
||||||
|
|
||||||
|
Errors reported by the device are raised as SmartDeviceExceptions,
|
||||||
|
and should be handled by the user of the library.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The library references the same structure as defined for the D-Link Switch
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
host: str,
|
||||||
|
protocol: 'TPLinkSmartHomeProtocol' = None) -> None:
|
||||||
|
SmartPlug.__init__(self, host, protocol)
|
||||||
|
self.emeter_type = "emeter"
|
||||||
|
self.plugs = {}
|
||||||
|
children = self.sys_info["children"]
|
||||||
|
self.num_children = len(children)
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
self.plugs[plug] = SmartPlug(host, protocol,
|
||||||
|
context=children[plug]["id"])
|
||||||
|
|
||||||
|
def raise_for_index(self, index: int):
|
||||||
|
"""
|
||||||
|
Raises SmartStripException if the plug index is out of bounds
|
||||||
|
|
||||||
|
:param index: plug index to check
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if index not in range(self.num_children):
|
||||||
|
raise SmartStripException("plug index of %d "
|
||||||
|
"is out of bounds" % index)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Dict[int, str]:
|
||||||
|
"""
|
||||||
|
Retrieve the switch state
|
||||||
|
|
||||||
|
:returns: list with the state of each child plug
|
||||||
|
SWITCH_STATE_ON
|
||||||
|
SWITCH_STATE_OFF
|
||||||
|
SWITCH_STATE_UNKNOWN
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
states = {}
|
||||||
|
children = self.sys_info["children"]
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
relay_state = children[plug]["state"]
|
||||||
|
|
||||||
|
if relay_state == 0:
|
||||||
|
switch_state = SmartPlug.SWITCH_STATE_OFF
|
||||||
|
elif relay_state == 1:
|
||||||
|
switch_state = SmartPlug.SWITCH_STATE_ON
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Unknown state %s returned for plug %u.",
|
||||||
|
relay_state, plug)
|
||||||
|
switch_state = SmartPlug.SWITCH_STATE_UNKNOWN
|
||||||
|
|
||||||
|
states[plug] = switch_state
|
||||||
|
|
||||||
|
return states
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, value: str):
|
||||||
|
"""
|
||||||
|
Sets the state of all plugs in the strip
|
||||||
|
|
||||||
|
:param value: one of
|
||||||
|
SWITCH_STATE_ON
|
||||||
|
SWITCH_STATE_OFF
|
||||||
|
:raises ValueError: on invalid state
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
"""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise ValueError("State must be str, not of %s.", type(value))
|
||||||
|
elif value.upper() == SmartPlug.SWITCH_STATE_ON:
|
||||||
|
self.turn_on()
|
||||||
|
elif value.upper() == SmartPlug.SWITCH_STATE_OFF:
|
||||||
|
self.turn_off()
|
||||||
|
else:
|
||||||
|
raise ValueError("State %s is not valid.", value)
|
||||||
|
|
||||||
|
def set_state(self, value: str, *, index: int = -1):
|
||||||
|
"""
|
||||||
|
Sets the state of a plug on the strip
|
||||||
|
|
||||||
|
:param value: one of
|
||||||
|
SWITCH_STATE_ON
|
||||||
|
SWITCH_STATE_OFF
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:raises ValueError: on invalid state
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if index < 0:
|
||||||
|
self.state = value
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
self.plugs[index].state = value
|
||||||
|
|
||||||
|
def is_on(self, *, index: int = -1) -> Any:
|
||||||
|
"""
|
||||||
|
Returns whether device is on.
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:return: True if device is on, False otherwise, Dict without index
|
||||||
|
:rtype: bool if index is provided
|
||||||
|
Dict[int, bool] if no index provided
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
children = self.sys_info["children"]
|
||||||
|
if index < 0:
|
||||||
|
is_on = {}
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
is_on[plug] = bool(children[plug]["state"])
|
||||||
|
return is_on
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
return bool(children[index]["state"])
|
||||||
|
|
||||||
|
def turn_on(self, *, index: int = -1):
|
||||||
|
"""
|
||||||
|
Turns outlets on
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if index < 0:
|
||||||
|
self._query_helper("system", "set_relay_state", {"state": 1})
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
self.plugs[index].turn_on()
|
||||||
|
|
||||||
|
def turn_off(self, *, index: int = -1):
|
||||||
|
"""
|
||||||
|
Turns outlets off
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if index < 0:
|
||||||
|
self._query_helper("system", "set_relay_state", {"state": 0})
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
self.plugs[index].turn_off()
|
||||||
|
|
||||||
|
def on_since(self, *, index: int = -1) -> Any:
|
||||||
|
"""
|
||||||
|
Returns pretty-printed on-time
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:return: datetime for on since
|
||||||
|
:rtype: datetime with index
|
||||||
|
Dict[int, str] without index
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if index < 0:
|
||||||
|
on_since = {}
|
||||||
|
children = self.sys_info["children"]
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
on_since[plug] = \
|
||||||
|
datetime.datetime.now() - \
|
||||||
|
datetime.timedelta(seconds=children[plug]["on_time"])
|
||||||
|
return on_since
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
return self.plugs[index].on_since
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_information(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Returns strip-specific state information.
|
||||||
|
|
||||||
|
:return: Strip information dict, keys in user-presentable form.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
state = {'LED state': self.led}
|
||||||
|
on_since = self.on_since()
|
||||||
|
for plug_index in range(self.num_children):
|
||||||
|
state['Plug %d on since' % (plug_index + 1)] = on_since[plug_index]
|
||||||
|
return state
|
||||||
|
|
||||||
|
def get_emeter_realtime(self, *, index: int = -1) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
Retrieve current energy readings from device
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:returns: list of current readings or None
|
||||||
|
:rtype: Dict, Dict[int, Dict], None
|
||||||
|
Dict if index is provided
|
||||||
|
Dict[int, Dict] if no index provided
|
||||||
|
None if device has no energy meter or error occurred
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if not self.has_emeter:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if index < 0:
|
||||||
|
emeter_status = {}
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
emeter_status[plug] = self.plugs[plug].get_emeter_realtime()
|
||||||
|
return emeter_status
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
return self.plugs[index].get_emeter_realtime()
|
||||||
|
|
||||||
|
def current_consumption(self, *, index: int = -1) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
Get the current power consumption in Watts.
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:return: the current power consumption in Watts.
|
||||||
|
None if device has no energy meter.
|
||||||
|
:rtype: Dict, Dict[int, Dict], None
|
||||||
|
Dict if index is provided
|
||||||
|
Dict[int, Dict] if no index provided
|
||||||
|
None if device has no energy meter or error occurred
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if not self.has_emeter:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if index < 0:
|
||||||
|
consumption = {}
|
||||||
|
emeter_reading = self.get_emeter_realtime()
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
response = EmeterStatus(emeter_reading[plug])
|
||||||
|
consumption[plug] = response["power"]
|
||||||
|
return consumption
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
response = EmeterStatus(self.get_emeter_realtime(index=index))
|
||||||
|
return response["power"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""
|
||||||
|
Override for base class icon property, SmartStrip and children do not
|
||||||
|
have icons.
|
||||||
|
|
||||||
|
:raises NotImplementedError: always
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("no icons for this device")
|
||||||
|
|
||||||
|
def get_alias(self, *, index: int = -1) -> Union[str, Dict[int, str]]:
|
||||||
|
"""
|
||||||
|
Gets the alias for a plug.
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:return: the current power consumption in Watts.
|
||||||
|
None if device has no energy meter.
|
||||||
|
:rtype: str if index is provided
|
||||||
|
Dict[int, str] if no index provided
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
children = self.sys_info["children"]
|
||||||
|
|
||||||
|
if index < 0:
|
||||||
|
alias = {}
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
alias[plug] = children[plug]["alias"]
|
||||||
|
return alias
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
return children[index]["alias"]
|
||||||
|
|
||||||
|
def set_alias(self, alias: str, index: int):
|
||||||
|
"""
|
||||||
|
Sets the alias for a plug
|
||||||
|
|
||||||
|
:param index: plug index
|
||||||
|
:param alias: new alias
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
self.raise_for_index(index)
|
||||||
|
self.plugs[index].alias = alias
|
||||||
|
|
||||||
|
def get_emeter_daily(self,
|
||||||
|
year: int = None,
|
||||||
|
month: int = None,
|
||||||
|
kwh: bool = True,
|
||||||
|
*,
|
||||||
|
index: int = -1) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Retrieve daily statistics for a given month
|
||||||
|
|
||||||
|
:param year: year for which to retrieve statistics (default: this year)
|
||||||
|
:param month: month for which to retrieve statistics (default: this
|
||||||
|
month)
|
||||||
|
:param kwh: return usage in kWh (default: True)
|
||||||
|
:return: mapping of day of month to value
|
||||||
|
None if device has no energy meter or error occurred
|
||||||
|
:rtype: dict
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if not self.has_emeter:
|
||||||
|
return None
|
||||||
|
|
||||||
|
emeter_daily = {}
|
||||||
|
if index < 0:
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
emeter_daily = self.plugs[plug].get_emeter_daily(year=year,
|
||||||
|
month=month,
|
||||||
|
kwh=kwh)
|
||||||
|
return emeter_daily
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
return self.plugs[index].get_emeter_daily(year=year,
|
||||||
|
month=month,
|
||||||
|
kwh=kwh)
|
||||||
|
|
||||||
|
def get_emeter_monthly(self,
|
||||||
|
year: int = None,
|
||||||
|
kwh: bool = True,
|
||||||
|
*,
|
||||||
|
index: int = -1) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Retrieve monthly statistics for a given year.
|
||||||
|
|
||||||
|
:param year: year for which to retrieve statistics (default: this year)
|
||||||
|
:param kwh: return usage in kWh (default: True)
|
||||||
|
:return: dict: mapping of month to value
|
||||||
|
None if device has no energy meter
|
||||||
|
:rtype: dict
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if not self.has_emeter:
|
||||||
|
return None
|
||||||
|
|
||||||
|
emeter_monthly = {}
|
||||||
|
if index < 0:
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
emeter_monthly = self.plugs[plug].get_emeter_monthly(year=year,
|
||||||
|
kwh=kwh)
|
||||||
|
return emeter_monthly
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
return self.plugs[index].get_emeter_monthly(year=year,
|
||||||
|
kwh=kwh)
|
||||||
|
|
||||||
|
def erase_emeter_stats(self, *, index: int = -1) -> bool:
|
||||||
|
"""
|
||||||
|
Erase energy meter statistics
|
||||||
|
|
||||||
|
:param index: plug index (-1 for all)
|
||||||
|
:return: True if statistics were deleted
|
||||||
|
False if device has no energy meter.
|
||||||
|
:rtype: bool
|
||||||
|
:raises SmartDeviceException: on error
|
||||||
|
:raises SmartStripException: index out of bounds
|
||||||
|
"""
|
||||||
|
if not self.has_emeter:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if index < 0:
|
||||||
|
for plug in range(self.num_children):
|
||||||
|
self.plugs[plug].erase_emeter_stats()
|
||||||
|
else:
|
||||||
|
self.raise_for_index(index)
|
||||||
|
self.plugs[index].erase_emeter_stats()
|
||||||
|
|
||||||
|
# As query_helper raises exception in case of failure, we have
|
||||||
|
# succeeded when we are this far.
|
||||||
|
return True
|
@ -5,35 +5,42 @@ import logging
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def get_realtime(obj, x):
|
|
||||||
|
def get_realtime(obj, x, child_ids=[]):
|
||||||
return {"current":0.268587,"voltage":125.836131,"power":33.495623,"total":0.199000}
|
return {"current":0.268587,"voltage":125.836131,"power":33.495623,"total":0.199000}
|
||||||
|
|
||||||
def get_monthstat(obj, x):
|
|
||||||
|
def get_monthstat(obj, x, child_ids=[]):
|
||||||
if x["year"] < 2016:
|
if x["year"] < 2016:
|
||||||
return {"month_list":[]}
|
return {"month_list":[]}
|
||||||
|
|
||||||
return {"month_list": [{"year": 2016, "month": 11, "energy": 1.089000}, {"year": 2016, "month": 12, "energy": 1.582000}]}
|
return {"month_list": [{"year": 2016, "month": 11, "energy": 1.089000}, {"year": 2016, "month": 12, "energy": 1.582000}]}
|
||||||
|
|
||||||
def get_daystat(obj, x):
|
|
||||||
|
def get_daystat(obj, x, child_ids=[]):
|
||||||
if x["year"] < 2016:
|
if x["year"] < 2016:
|
||||||
return {"day_list":[]}
|
return {"day_list":[]}
|
||||||
|
|
||||||
return {"day_list": [{"year": 2016, "month": 11, "day": 24, "energy": 0.026000},
|
return {"day_list": [{"year": 2016, "month": 11, "day": 24, "energy": 0.026000},
|
||||||
{"year": 2016, "month": 11, "day": 25, "energy": 0.109000}]}
|
{"year": 2016, "month": 11, "day": 25, "energy": 0.109000}]}
|
||||||
|
|
||||||
|
|
||||||
emeter_support = {"get_realtime": get_realtime,
|
emeter_support = {"get_realtime": get_realtime,
|
||||||
"get_monthstat": get_monthstat,
|
"get_monthstat": get_monthstat,
|
||||||
"get_daystat": get_daystat,}
|
"get_daystat": get_daystat,}
|
||||||
|
|
||||||
|
|
||||||
def get_realtime_units(obj, x):
|
def get_realtime_units(obj, x):
|
||||||
return {"power_mw": 10800}
|
return {"power_mw": 10800}
|
||||||
|
|
||||||
|
|
||||||
def get_monthstat_units(obj, x):
|
def get_monthstat_units(obj, x):
|
||||||
if x["year"] < 2016:
|
if x["year"] < 2016:
|
||||||
return {"month_list":[]}
|
return {"month_list":[]}
|
||||||
|
|
||||||
return {"month_list": [{"year": 2016, "month": 11, "energy_wh": 32}, {"year": 2016, "month": 12, "energy_wh": 16}]}
|
return {"month_list": [{"year": 2016, "month": 11, "energy_wh": 32}, {"year": 2016, "month": 12, "energy_wh": 16}]}
|
||||||
|
|
||||||
|
|
||||||
def get_daystat_units(obj, x):
|
def get_daystat_units(obj, x):
|
||||||
if x["year"] < 2016:
|
if x["year"] < 2016:
|
||||||
return {"day_list":[]}
|
return {"day_list":[]}
|
||||||
@ -41,10 +48,91 @@ def get_daystat_units(obj, x):
|
|||||||
return {"day_list": [{"year": 2016, "month": 11, "day": 24, "energy_wh": 20},
|
return {"day_list": [{"year": 2016, "month": 11, "day": 24, "energy_wh": 20},
|
||||||
{"year": 2016, "month": 11, "day": 25, "energy_wh": 32}]}
|
{"year": 2016, "month": 11, "day": 25, "energy_wh": 32}]}
|
||||||
|
|
||||||
|
|
||||||
emeter_units_support = {"get_realtime": get_realtime_units,
|
emeter_units_support = {"get_realtime": get_realtime_units,
|
||||||
"get_monthstat": get_monthstat_units,
|
"get_monthstat": get_monthstat_units,
|
||||||
"get_daystat": get_daystat_units,}
|
"get_daystat": get_daystat_units,}
|
||||||
|
|
||||||
|
sysinfo_hs300 = {
|
||||||
|
'system': {
|
||||||
|
'get_sysinfo': {
|
||||||
|
'sw_ver': '1.0.6 Build 180627 Rel.081000',
|
||||||
|
'hw_ver': '1.0',
|
||||||
|
'model': 'HS300(US)',
|
||||||
|
'deviceId': '7003ADE7030B7EFADE747104261A7A70931DADF4',
|
||||||
|
'oemId': 'FFF22CFF774A0B89F7624BFC6F50D5DE',
|
||||||
|
'hwId': '22603EA5E716DEAEA6642A30BE87AFCB',
|
||||||
|
'rssi': -53,
|
||||||
|
'longitude_i': -1198698,
|
||||||
|
'latitude_i': 352737,
|
||||||
|
'alias': 'TP-LINK_Power Strip_2233',
|
||||||
|
'mic_type': 'IOT.SMARTPLUGSWITCH',
|
||||||
|
'feature': 'TIM:ENE',
|
||||||
|
'mac': '50:C7:BF:11:22:33',
|
||||||
|
'updating': 0,
|
||||||
|
'led_off': 0,
|
||||||
|
'children': [
|
||||||
|
{
|
||||||
|
'id': '7003ADE7030B7EFADE747104261A7A70931DADF400',
|
||||||
|
'state': 1,
|
||||||
|
'alias': 'my plug 1 device',
|
||||||
|
'on_time': 5423,
|
||||||
|
'next_action': {
|
||||||
|
'type': -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '7003ADE7030B7EFADE747104261A7A70931DADF401',
|
||||||
|
'state': 1,
|
||||||
|
'alias': 'my plug 2 device',
|
||||||
|
'on_time': 4750,
|
||||||
|
'next_action': {
|
||||||
|
'type': -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '7003ADE7030B7EFADE747104261A7A70931DADF402',
|
||||||
|
'state': 1,
|
||||||
|
'alias': 'my plug 3 device',
|
||||||
|
'on_time': 4748,
|
||||||
|
'next_action': {
|
||||||
|
'type': -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '7003ADE7030B7EFADE747104261A7A70931DADF403',
|
||||||
|
'state': 1,
|
||||||
|
'alias': 'my plug 4 device',
|
||||||
|
'on_time': 4742,
|
||||||
|
'next_action': {
|
||||||
|
'type': -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '7003ADE7030B7EFADE747104261A7A70931DADF404',
|
||||||
|
'state': 1,
|
||||||
|
'alias': 'my plug 5 device',
|
||||||
|
'on_time': 4745,
|
||||||
|
'next_action': {
|
||||||
|
'type': -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '7003ADE7030B7EFADE747104261A7A70931DADF405',
|
||||||
|
'state': 1,
|
||||||
|
'alias': 'my plug 6 device',
|
||||||
|
'on_time': 5028,
|
||||||
|
'next_action': {
|
||||||
|
'type': -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'child_num': 6,
|
||||||
|
'err_code': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sysinfo_hs100 = {'system': {'get_sysinfo':
|
sysinfo_hs100 = {'system': {'get_sysinfo':
|
||||||
{'active_mode': 'schedule',
|
{'active_mode': 'schedule',
|
||||||
'alias': 'My Smart Plug',
|
'alias': 'My Smart Plug',
|
||||||
@ -484,13 +572,28 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.invalid = invalid
|
self.invalid = invalid
|
||||||
|
|
||||||
def set_alias(self, x):
|
def set_alias(self, x, child_ids=[]):
|
||||||
_LOGGER.debug("Setting alias to %s", x["alias"])
|
_LOGGER.debug("Setting alias to %s", x["alias"])
|
||||||
self.proto["system"]["get_sysinfo"]["alias"] = x["alias"]
|
if child_ids:
|
||||||
|
for child in self.proto["system"]["get_sysinfo"]["children"]:
|
||||||
|
if child["id"] in child_ids:
|
||||||
|
child["alias"] = x["alias"]
|
||||||
|
else:
|
||||||
|
self.proto["system"]["get_sysinfo"]["alias"] = x["alias"]
|
||||||
|
|
||||||
def set_relay_state(self, x):
|
def set_relay_state(self, x, child_ids=[]):
|
||||||
_LOGGER.debug("Setting relay state to %s", x)
|
_LOGGER.debug("Setting relay state to %s", x["state"])
|
||||||
self.proto["system"]["get_sysinfo"]["relay_state"] = x["state"]
|
|
||||||
|
if not child_ids and "children" in self.proto["system"]["get_sysinfo"]:
|
||||||
|
for child in self.proto["system"]["get_sysinfo"]["children"]:
|
||||||
|
child_ids.append(child["id"])
|
||||||
|
|
||||||
|
if child_ids:
|
||||||
|
for child in self.proto["system"]["get_sysinfo"]["children"]:
|
||||||
|
if child["id"] in child_ids:
|
||||||
|
child["state"] = x["state"]
|
||||||
|
else:
|
||||||
|
self.proto["system"]["get_sysinfo"]["relay_state"] = x["state"]
|
||||||
|
|
||||||
def set_led_off(self, x):
|
def set_led_off(self, x):
|
||||||
_LOGGER.debug("Setting led off to %s", x)
|
_LOGGER.debug("Setting led off to %s", x)
|
||||||
@ -516,6 +619,7 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
"get_dev_icon": {"icon": None, "hash": None},
|
"get_dev_icon": {"icon": None, "hash": None},
|
||||||
"set_mac_addr": set_mac,
|
"set_mac_addr": set_mac,
|
||||||
"get_sysinfo": None,
|
"get_sysinfo": None,
|
||||||
|
"context": None,
|
||||||
},
|
},
|
||||||
"emeter": { "get_realtime": None,
|
"emeter": { "get_realtime": None,
|
||||||
"get_daystat": None,
|
"get_daystat": None,
|
||||||
@ -537,14 +641,24 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
# HS220 brightness, different setter and getter
|
# HS220 brightness, different setter and getter
|
||||||
"smartlife.iot.dimmer": { "set_brightness": set_hs220_brightness,
|
"smartlife.iot.dimmer": { "set_brightness": set_hs220_brightness,
|
||||||
},
|
},
|
||||||
|
"context": {"child_ids": None},
|
||||||
}
|
}
|
||||||
|
|
||||||
def query(self, host, request, port=9999):
|
def query(self, host, request, port=9999):
|
||||||
if self.invalid:
|
if self.invalid:
|
||||||
raise SmartDeviceException("Invalid connection, can't query!")
|
raise SmartDeviceException("Invalid connection, can't query!")
|
||||||
|
|
||||||
|
_LOGGER.debug("Requesting {} from {}:{}".format(request, host, port))
|
||||||
|
|
||||||
proto = self.proto
|
proto = self.proto
|
||||||
|
|
||||||
|
# collect child ids from context
|
||||||
|
try:
|
||||||
|
child_ids = request["context"]["child_ids"]
|
||||||
|
request.pop("context", None)
|
||||||
|
except KeyError:
|
||||||
|
child_ids = []
|
||||||
|
|
||||||
target = next(iter(request))
|
target = next(iter(request))
|
||||||
if target not in proto.keys():
|
if target not in proto.keys():
|
||||||
return error(target, msg="target not found")
|
return error(target, msg="target not found")
|
||||||
@ -557,7 +671,10 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
_LOGGER.debug("Going to execute {}.{} (params: {}).. ".format(target, cmd, params))
|
_LOGGER.debug("Going to execute {}.{} (params: {}).. ".format(target, cmd, params))
|
||||||
|
|
||||||
if callable(proto[target][cmd]):
|
if callable(proto[target][cmd]):
|
||||||
res = proto[target][cmd](self, params)
|
if child_ids:
|
||||||
|
res = proto[target][cmd](self, params, child_ids)
|
||||||
|
else:
|
||||||
|
res = proto[target][cmd](self, params)
|
||||||
# verify that change didn't break schema, requires refactoring..
|
# verify that change didn't break schema, requires refactoring..
|
||||||
#TestSmartPlug.sysinfo_schema(self.proto["system"]["get_sysinfo"])
|
#TestSmartPlug.sysinfo_schema(self.proto["system"]["get_sysinfo"])
|
||||||
return success(target, cmd, res)
|
return success(target, cmd, res)
|
||||||
|
@ -84,7 +84,7 @@ class TestSmartPlugHS100(TestCase):
|
|||||||
'total': Any(Coerce(float, Range(min=0)), None),
|
'total': Any(Coerce(float, Range(min=0)), None),
|
||||||
'current': Any(All(float, Range(min=0)), None),
|
'current': Any(All(float, Range(min=0)), None),
|
||||||
|
|
||||||
'voltage_mw': Any(All(float, Range(min=0, max=300000)), None),
|
'voltage_mv': Any(All(float, Range(min=0, max=300000)), None),
|
||||||
'power_mw': Any(Coerce(float, Range(min=0)), None),
|
'power_mw': Any(Coerce(float, Range(min=0)), None),
|
||||||
'total_wh': Any(Coerce(float, Range(min=0)), None),
|
'total_wh': Any(Coerce(float, Range(min=0)), None),
|
||||||
'current_ma': Any(All(float, Range(min=0)), None),
|
'current_ma': Any(All(float, Range(min=0)), None),
|
||||||
|
438
pyHS100/tests/test_strip.py
Normal file
438
pyHS100/tests/test_strip.py
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
from unittest import TestCase, skip
|
||||||
|
from voluptuous import Schema, All, Any, Range, Coerce
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from .. import SmartStrip, SmartPlug, SmartStripException, SmartDeviceException
|
||||||
|
from .fakes import FakeTransportProtocol, sysinfo_hs300
|
||||||
|
from .test_pyHS100 import check_mac, check_int_bool
|
||||||
|
|
||||||
|
# Set IP instead of None if you want to run tests on a device.
|
||||||
|
STRIP_IP = None
|
||||||
|
|
||||||
|
|
||||||
|
class TestSmartStripHS300(TestCase):
|
||||||
|
SYSINFO = sysinfo_hs300 # type: Dict
|
||||||
|
# these schemas should go to the mainlib as
|
||||||
|
# they can be useful when adding support for new features/devices
|
||||||
|
# as well as to check that faked devices are operating properly.
|
||||||
|
sysinfo_schema = Schema({
|
||||||
|
"sw_ver": str,
|
||||||
|
"hw_ver": str,
|
||||||
|
"model": str,
|
||||||
|
"deviceId": str,
|
||||||
|
"oemId": str,
|
||||||
|
"hwId": str,
|
||||||
|
"rssi": Any(int, None), # rssi can also be positive, see #54
|
||||||
|
"longitude": Any(All(int, Range(min=-1800000, max=1800000)), None),
|
||||||
|
"latitude": Any(All(int, Range(min=-900000, max=900000)), None),
|
||||||
|
"longitude_i": Any(All(int, Range(min=-1800000, max=1800000)), None),
|
||||||
|
"latitude_i": Any(All(int, Range(min=-900000, max=900000)), None),
|
||||||
|
"alias": str,
|
||||||
|
"mic_type": str,
|
||||||
|
"feature": str,
|
||||||
|
"mac": check_mac,
|
||||||
|
"updating": check_int_bool,
|
||||||
|
"led_off": check_int_bool,
|
||||||
|
"children": [{
|
||||||
|
"id": str,
|
||||||
|
"state": int,
|
||||||
|
"alias": str,
|
||||||
|
"on_time": int,
|
||||||
|
"next_action": {"type": int},
|
||||||
|
}],
|
||||||
|
"child_num": int,
|
||||||
|
"err_code": int,
|
||||||
|
})
|
||||||
|
|
||||||
|
current_consumption_schema = Schema(
|
||||||
|
Any(
|
||||||
|
{
|
||||||
|
"voltage": Any(All(float, Range(min=0, max=300)), None),
|
||||||
|
"power": Any(Coerce(float, Range(min=0)), None),
|
||||||
|
"total": Any(Coerce(float, Range(min=0)), None),
|
||||||
|
"current": Any(All(float, Range(min=0)), None),
|
||||||
|
|
||||||
|
"voltage_mv": Any(All(int, Range(min=0, max=300000)), None),
|
||||||
|
"power_mw": Any(Coerce(int, Range(min=0)), None),
|
||||||
|
"total_wh": Any(Coerce(int, Range(min=0)), None),
|
||||||
|
"current_ma": Any(All(int, Range(min=0)), None),
|
||||||
|
},
|
||||||
|
None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
tz_schema = Schema({
|
||||||
|
"zone_str": str,
|
||||||
|
"dst_offset": int,
|
||||||
|
"index": All(int, Range(min=0)),
|
||||||
|
"tz_str": str,
|
||||||
|
})
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if STRIP_IP is not None:
|
||||||
|
self.strip = SmartStrip(STRIP_IP)
|
||||||
|
else:
|
||||||
|
self.strip = SmartStrip(
|
||||||
|
host="127.0.0.1",
|
||||||
|
protocol=FakeTransportProtocol(self.SYSINFO)
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.strip = None
|
||||||
|
|
||||||
|
def test_initialize(self):
|
||||||
|
self.assertIsNotNone(self.strip.sys_info)
|
||||||
|
self.assertTrue(self.strip.num_children)
|
||||||
|
self.sysinfo_schema(self.strip.sys_info)
|
||||||
|
|
||||||
|
def test_initialize_invalid_connection(self):
|
||||||
|
with self.assertRaises(SmartDeviceException):
|
||||||
|
SmartStrip(
|
||||||
|
host="127.0.0.1",
|
||||||
|
protocol=FakeTransportProtocol(self.SYSINFO, invalid=True))
|
||||||
|
|
||||||
|
def test_query_helper(self):
|
||||||
|
with self.assertRaises(SmartDeviceException):
|
||||||
|
self.strip._query_helper("test", "testcmd", {})
|
||||||
|
|
||||||
|
def test_raise_for_index(self):
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.raise_for_index(index=self.strip.num_children + 100)
|
||||||
|
|
||||||
|
def test_state_strip(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.strip.state = 1234
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.strip.state = "1234"
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.strip.state = True
|
||||||
|
|
||||||
|
orig_state = self.strip.state
|
||||||
|
if orig_state == SmartPlug.SWITCH_STATE_OFF:
|
||||||
|
self.strip.state = "ON"
|
||||||
|
self.assertTrue(self.strip.state == SmartPlug.SWITCH_STATE_ON)
|
||||||
|
self.strip.state = "OFF"
|
||||||
|
self.assertTrue(self.strip.state == SmartPlug.SWITCH_STATE_OFF)
|
||||||
|
elif orig_state == SmartPlug.SWITCH_STATE_ON:
|
||||||
|
self.strip.state = "OFF"
|
||||||
|
self.assertTrue(self.strip.state == SmartPlug.SWITCH_STATE_OFF)
|
||||||
|
self.strip.state = "ON"
|
||||||
|
self.assertTrue(self.strip.state == SmartPlug.SWITCH_STATE_ON)
|
||||||
|
elif orig_state == SmartPlug.SWITCH_STATE_UNKNOWN:
|
||||||
|
self.fail("can't test for unknown state")
|
||||||
|
|
||||||
|
def test_state_plugs(self):
|
||||||
|
# value errors
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.strip.set_state(value=1234, index=plug_index)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.strip.set_state(value="1234", index=plug_index)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.strip.set_state(value=True, index=plug_index)
|
||||||
|
|
||||||
|
# out of bounds error
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.set_state(
|
||||||
|
value=SmartPlug.SWITCH_STATE_ON,
|
||||||
|
index=self.strip.num_children + 100
|
||||||
|
)
|
||||||
|
|
||||||
|
# on off
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
orig_state = self.strip.state[plug_index]
|
||||||
|
if orig_state == SmartPlug.SWITCH_STATE_OFF:
|
||||||
|
self.strip.set_state(value="ON", index=plug_index)
|
||||||
|
self.assertTrue(
|
||||||
|
self.strip.state[plug_index] == SmartPlug.SWITCH_STATE_ON)
|
||||||
|
self.strip.set_state(value="OFF", index=plug_index)
|
||||||
|
self.assertTrue(
|
||||||
|
self.strip.state[plug_index] == SmartPlug.SWITCH_STATE_OFF)
|
||||||
|
elif orig_state == SmartPlug.SWITCH_STATE_ON:
|
||||||
|
self.strip.set_state(value="OFF", index=plug_index)
|
||||||
|
self.assertTrue(
|
||||||
|
self.strip.state[plug_index] == SmartPlug.SWITCH_STATE_OFF)
|
||||||
|
self.strip.set_state(value="ON", index=plug_index)
|
||||||
|
self.assertTrue(
|
||||||
|
self.strip.state[plug_index] == SmartPlug.SWITCH_STATE_ON)
|
||||||
|
elif orig_state == SmartPlug.SWITCH_STATE_UNKNOWN:
|
||||||
|
self.fail("can't test for unknown state")
|
||||||
|
|
||||||
|
def test_turns_and_isses(self):
|
||||||
|
# all on
|
||||||
|
self.strip.turn_on()
|
||||||
|
for index, state in self.strip.is_on().items():
|
||||||
|
self.assertTrue(state)
|
||||||
|
self.assertTrue(self.strip.is_on(index=index) == state)
|
||||||
|
|
||||||
|
# all off
|
||||||
|
self.strip.turn_off()
|
||||||
|
for index, state in self.strip.is_on().items():
|
||||||
|
self.assertFalse(state)
|
||||||
|
self.assertTrue(self.strip.is_on(index=index) == state)
|
||||||
|
|
||||||
|
# individual on
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
original_states = self.strip.is_on()
|
||||||
|
self.strip.turn_on(index=plug_index)
|
||||||
|
|
||||||
|
# only target outlet should have state changed
|
||||||
|
for index, state in self.strip.is_on().items():
|
||||||
|
if index == plug_index:
|
||||||
|
self.assertTrue(state != original_states[index])
|
||||||
|
else:
|
||||||
|
self.assertTrue(state == original_states[index])
|
||||||
|
|
||||||
|
# individual off
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
original_states = self.strip.is_on()
|
||||||
|
self.strip.turn_off(index=plug_index)
|
||||||
|
|
||||||
|
# only target outlet should have state changed
|
||||||
|
for index, state in self.strip.is_on().items():
|
||||||
|
if index == plug_index:
|
||||||
|
self.assertTrue(state != original_states[index])
|
||||||
|
else:
|
||||||
|
self.assertTrue(state == original_states[index])
|
||||||
|
|
||||||
|
# out of bounds
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.turn_off(index=self.strip.num_children + 100)
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.turn_on(index=self.strip.num_children + 100)
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.is_on(index=self.strip.num_children + 100)
|
||||||
|
|
||||||
|
@skip("this test will wear out your relays")
|
||||||
|
def test_all_binary_states(self):
|
||||||
|
# test every binary state
|
||||||
|
for state in range(2 ** self.strip.num_children):
|
||||||
|
|
||||||
|
# create binary state map
|
||||||
|
state_map = {}
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
state_map[plug_index] = bool((state >> plug_index) & 1)
|
||||||
|
|
||||||
|
if state_map[plug_index]:
|
||||||
|
self.strip.turn_on(index=plug_index)
|
||||||
|
else:
|
||||||
|
self.strip.turn_off(index=plug_index)
|
||||||
|
|
||||||
|
# check state map applied
|
||||||
|
for index, state in self.strip.is_on().items():
|
||||||
|
self.assertTrue(state_map[index] == state)
|
||||||
|
|
||||||
|
# toggle each outlet with state map applied
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
|
||||||
|
# toggle state
|
||||||
|
if state_map[plug_index]:
|
||||||
|
self.strip.turn_off(index=plug_index)
|
||||||
|
else:
|
||||||
|
self.strip.turn_on(index=plug_index)
|
||||||
|
|
||||||
|
# only target outlet should have state changed
|
||||||
|
for index, state in self.strip.is_on().items():
|
||||||
|
if index == plug_index:
|
||||||
|
self.assertTrue(state != state_map[index])
|
||||||
|
else:
|
||||||
|
self.assertTrue(state == state_map[index])
|
||||||
|
|
||||||
|
# reset state
|
||||||
|
if state_map[plug_index]:
|
||||||
|
self.strip.turn_on(index=plug_index)
|
||||||
|
else:
|
||||||
|
self.strip.turn_off(index=plug_index)
|
||||||
|
|
||||||
|
# original state map should be restored
|
||||||
|
for index, state in self.strip.is_on().items():
|
||||||
|
self.assertTrue(state == state_map[index])
|
||||||
|
|
||||||
|
def test_has_emeter(self):
|
||||||
|
# a not so nice way for checking for emeter availability..
|
||||||
|
if "HS300" in self.strip.sys_info["model"]:
|
||||||
|
self.assertTrue(self.strip.has_emeter)
|
||||||
|
else:
|
||||||
|
self.assertFalse(self.strip.has_emeter)
|
||||||
|
|
||||||
|
def test_get_emeter_realtime(self):
|
||||||
|
if self.strip.has_emeter:
|
||||||
|
# test with index
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
emeter = self.strip.get_emeter_realtime(index=plug_index)
|
||||||
|
self.current_consumption_schema(emeter)
|
||||||
|
|
||||||
|
# test without index
|
||||||
|
for index, emeter in self.strip.get_emeter_realtime().items():
|
||||||
|
self.current_consumption_schema(emeter)
|
||||||
|
|
||||||
|
# out of bounds
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.get_emeter_realtime(
|
||||||
|
index=self.strip.num_children + 100
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(self.strip.get_emeter_realtime(), None)
|
||||||
|
|
||||||
|
def test_get_emeter_daily(self):
|
||||||
|
if self.strip.has_emeter:
|
||||||
|
# test with index
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
emeter = self.strip.get_emeter_daily(year=1900, month=1,
|
||||||
|
index=plug_index)
|
||||||
|
self.assertEqual(emeter, {})
|
||||||
|
if len(emeter) < 1:
|
||||||
|
print("no emeter daily information, skipping..")
|
||||||
|
return
|
||||||
|
k, v = emeter.popitem()
|
||||||
|
self.assertTrue(isinstance(k, int))
|
||||||
|
self.assertTrue(isinstance(v, float))
|
||||||
|
|
||||||
|
# test without index
|
||||||
|
all_emeter = self.strip.get_emeter_daily(year=1900, month=1)
|
||||||
|
for index, emeter in all_emeter.items():
|
||||||
|
self.assertEqual(emeter, {})
|
||||||
|
if len(emeter) < 1:
|
||||||
|
print("no emeter daily information, skipping..")
|
||||||
|
return
|
||||||
|
k, v = emeter.popitem()
|
||||||
|
self.assertTrue(isinstance(k, int))
|
||||||
|
self.assertTrue(isinstance(v, float))
|
||||||
|
|
||||||
|
# out of bounds
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.get_emeter_daily(
|
||||||
|
year=1900,
|
||||||
|
month=1,
|
||||||
|
index=self.strip.num_children + 100
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(
|
||||||
|
self.strip.get_emeter_daily(year=1900, month=1), None)
|
||||||
|
|
||||||
|
def test_get_emeter_monthly(self):
|
||||||
|
if self.strip.has_emeter:
|
||||||
|
# test with index
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
emeter = self.strip.get_emeter_monthly(year=1900,
|
||||||
|
index=plug_index)
|
||||||
|
self.assertEqual(emeter, {})
|
||||||
|
if len(emeter) < 1:
|
||||||
|
print("no emeter daily information, skipping..")
|
||||||
|
return
|
||||||
|
k, v = emeter.popitem()
|
||||||
|
self.assertTrue(isinstance(k, int))
|
||||||
|
self.assertTrue(isinstance(v, float))
|
||||||
|
|
||||||
|
# test without index
|
||||||
|
all_emeter = self.strip.get_emeter_monthly(year=1900)
|
||||||
|
for index, emeter in all_emeter.items():
|
||||||
|
self.assertEqual(emeter, {})
|
||||||
|
if len(emeter) < 1:
|
||||||
|
print("no emeter daily information, skipping..")
|
||||||
|
return
|
||||||
|
k, v = emeter.popitem()
|
||||||
|
self.assertTrue(isinstance(k, int))
|
||||||
|
self.assertTrue(isinstance(v, float))
|
||||||
|
|
||||||
|
# out of bounds
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.get_emeter_monthly(
|
||||||
|
year=1900,
|
||||||
|
index=self.strip.num_children + 100
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(self.strip.get_emeter_monthly(year=1900), None)
|
||||||
|
|
||||||
|
@skip("not clearing your stats..")
|
||||||
|
def test_erase_emeter_stats(self):
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
def test_current_consumption(self):
|
||||||
|
if self.strip.has_emeter:
|
||||||
|
# test with index
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
emeter = self.strip.current_consumption(index=plug_index)
|
||||||
|
self.assertTrue(isinstance(emeter, float))
|
||||||
|
self.assertTrue(emeter >= 0.0)
|
||||||
|
|
||||||
|
# test without index
|
||||||
|
for index, emeter in self.strip.current_consumption().items():
|
||||||
|
self.assertTrue(isinstance(emeter, float))
|
||||||
|
self.assertTrue(emeter >= 0.0)
|
||||||
|
|
||||||
|
# out of bounds
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.current_consumption(
|
||||||
|
index=self.strip.num_children + 100
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(self.strip.current_consumption(), None)
|
||||||
|
|
||||||
|
def test_alias(self):
|
||||||
|
test_alias = "TEST1234"
|
||||||
|
|
||||||
|
# strip alias
|
||||||
|
original = self.strip.alias
|
||||||
|
self.assertTrue(isinstance(original, str))
|
||||||
|
self.strip.alias = test_alias
|
||||||
|
self.assertEqual(self.strip.alias, test_alias)
|
||||||
|
self.strip.alias = original
|
||||||
|
self.assertEqual(self.strip.alias, original)
|
||||||
|
|
||||||
|
# plug alias
|
||||||
|
original = self.strip.get_alias()
|
||||||
|
for plug in range(self.strip.num_children):
|
||||||
|
self.strip.set_alias(alias=test_alias, index=plug)
|
||||||
|
self.assertEqual(self.strip.get_alias(index=plug), test_alias)
|
||||||
|
self.strip.set_alias(alias=original[plug], index=plug)
|
||||||
|
self.assertEqual(self.strip.get_alias(index=plug), original[plug])
|
||||||
|
|
||||||
|
def test_led(self):
|
||||||
|
original = self.strip.led
|
||||||
|
|
||||||
|
self.strip.led = False
|
||||||
|
self.assertFalse(self.strip.led)
|
||||||
|
self.strip.led = True
|
||||||
|
self.assertTrue(self.strip.led)
|
||||||
|
self.strip.led = original
|
||||||
|
|
||||||
|
def test_icon(self):
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
self.strip.icon
|
||||||
|
|
||||||
|
def test_time(self):
|
||||||
|
self.assertTrue(isinstance(self.strip.time, datetime.datetime))
|
||||||
|
# TODO check setting?
|
||||||
|
|
||||||
|
def test_timezone(self):
|
||||||
|
self.tz_schema(self.strip.timezone)
|
||||||
|
|
||||||
|
def test_hw_info(self):
|
||||||
|
self.sysinfo_schema(self.strip.hw_info)
|
||||||
|
|
||||||
|
def test_on_since(self):
|
||||||
|
# out of bounds
|
||||||
|
with self.assertRaises(SmartStripException):
|
||||||
|
self.strip.on_since(index=self.strip.num_children + 1)
|
||||||
|
|
||||||
|
# individual on_since
|
||||||
|
for plug_index in range(self.strip.num_children):
|
||||||
|
self.assertTrue(isinstance(
|
||||||
|
self.strip.on_since(index=plug_index), datetime.datetime))
|
||||||
|
|
||||||
|
# all on_since
|
||||||
|
for index, plug_on_since in self.strip.on_since().items():
|
||||||
|
self.assertTrue(isinstance(plug_on_since, datetime.datetime))
|
||||||
|
|
||||||
|
def test_location(self):
|
||||||
|
print(self.strip.location)
|
||||||
|
self.sysinfo_schema(self.strip.location)
|
||||||
|
|
||||||
|
def test_rssi(self):
|
||||||
|
self.sysinfo_schema({'rssi': self.strip.rssi}) # wrapping for vol
|
||||||
|
|
||||||
|
def test_mac(self):
|
||||||
|
self.sysinfo_schema({'mac': self.strip.mac}) # wrapping for vol
|
@ -1,2 +1,2 @@
|
|||||||
click
|
click
|
||||||
click-datetime
|
click-datetime
|
||||||
|
Loading…
x
Reference in New Issue
Block a user