mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 03:33:35 +00:00
add typing hints to make it easier for 3rd party developers to use the library (#90)
* add typing hints to make it easier for 3rd party developers to use the library * remove unused devicetype enum to support python3.3 * add python 3.3 to travis and tox, install typing module in setup.py
This commit is contained in:
parent
3ddd31f3c1
commit
af90a36153
@ -1,6 +1,7 @@
|
|||||||
sudo: false
|
sudo: false
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
|
- "3.3"
|
||||||
- "3.4"
|
- "3.4"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.6"
|
- "3.6"
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
import socket
|
import socket
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from pyHS100 import TPLinkSmartHomeProtocol, SmartPlug, SmartBulb
|
from pyHS100 import TPLinkSmartHomeProtocol, SmartDevice, SmartPlug, SmartBulb
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Discover:
|
class Discover:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def discover(protocol=None, port=9999, timeout=3):
|
def discover(protocol: TPLinkSmartHomeProtocol = None,
|
||||||
|
port: int = 9999,
|
||||||
|
timeout: int = 3) -> Dict[str, SmartDevice]:
|
||||||
"""
|
"""
|
||||||
Sends discovery message to 255.255.255.255:9999 in order
|
Sends discovery message to 255.255.255.255:9999 in order
|
||||||
to detect available supported devices in the local network,
|
to detect available supported devices in the local network,
|
||||||
and waits for given timeout for answers from devices.
|
and waits for given timeout for answers from devices.
|
||||||
|
|
||||||
|
:param protocol: Protocol implementation to use
|
||||||
:param timeout: How long to wait for responses, defaults to 5
|
:param timeout: How long to wait for responses, defaults to 5
|
||||||
:param port: port to send broadcast messages, defaults to 9999.
|
:param port: port to send broadcast messages, defaults to 9999.
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
|
@ -2,6 +2,7 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -24,7 +25,9 @@ class TPLinkSmartHomeProtocol:
|
|||||||
DEFAULT_TIMEOUT = 5
|
DEFAULT_TIMEOUT = 5
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query(host, request, port=DEFAULT_PORT):
|
def query(host: str,
|
||||||
|
request: Union[str, Dict],
|
||||||
|
port: int = DEFAULT_PORT) -> Any:
|
||||||
"""
|
"""
|
||||||
Request information from a TP-Link SmartHome Device and return the
|
Request information from a TP-Link SmartHome Device and return the
|
||||||
response.
|
response.
|
||||||
@ -76,7 +79,7 @@ class TPLinkSmartHomeProtocol:
|
|||||||
return json.loads(response)
|
return json.loads(response)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encrypt(request):
|
def encrypt(request: str) -> bytearray:
|
||||||
"""
|
"""
|
||||||
Encrypt a request for a TP-Link Smart Home Device.
|
Encrypt a request for a TP-Link Smart Home Device.
|
||||||
|
|
||||||
@ -94,7 +97,7 @@ class TPLinkSmartHomeProtocol:
|
|||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decrypt(ciphertext):
|
def decrypt(ciphertext: bytes) -> str:
|
||||||
"""
|
"""
|
||||||
Decrypt a response of a TP-Link Smart Home Device.
|
Decrypt a response of a TP-Link Smart Home Device.
|
||||||
|
|
||||||
@ -104,9 +107,9 @@ class TPLinkSmartHomeProtocol:
|
|||||||
key = TPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
|
key = TPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
|
||||||
buffer = []
|
buffer = []
|
||||||
|
|
||||||
ciphertext = ciphertext.decode('latin-1')
|
ciphertext_str = ciphertext.decode('latin-1')
|
||||||
|
|
||||||
for char in ciphertext:
|
for char in ciphertext_str:
|
||||||
plain = key ^ ord(char)
|
plain = key ^ ord(char)
|
||||||
key = ord(char)
|
key = ord(char)
|
||||||
buffer.append(chr(plain))
|
buffer.append(chr(plain))
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from pyHS100 import SmartDevice
|
from pyHS100 import SmartDevice
|
||||||
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
class SmartBulb(SmartDevice):
|
class SmartBulb(SmartDevice):
|
||||||
@ -40,13 +41,15 @@ class SmartBulb(SmartDevice):
|
|||||||
BULB_STATE_ON = 'ON'
|
BULB_STATE_ON = 'ON'
|
||||||
BULB_STATE_OFF = 'OFF'
|
BULB_STATE_OFF = 'OFF'
|
||||||
|
|
||||||
def __init__(self, ip_address, protocol=None):
|
def __init__(self,
|
||||||
|
ip_address: str,
|
||||||
|
protocol: 'TPLinkSmartHomeProtocol' = None) -> None:
|
||||||
SmartDevice.__init__(self, ip_address, protocol)
|
SmartDevice.__init__(self, ip_address, protocol)
|
||||||
self.emeter_type = "smartlife.iot.common.emeter"
|
self.emeter_type = "smartlife.iot.common.emeter"
|
||||||
self.emeter_units = True
|
self.emeter_units = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_color(self):
|
def is_color(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Whether the bulb supports color changes
|
Whether the bulb supports color changes
|
||||||
|
|
||||||
@ -56,7 +59,7 @@ class SmartBulb(SmartDevice):
|
|||||||
return bool(self.sys_info['is_color'])
|
return bool(self.sys_info['is_color'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_dimmable(self):
|
def is_dimmable(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Whether the bulb supports brightness changes
|
Whether the bulb supports brightness changes
|
||||||
|
|
||||||
@ -66,7 +69,7 @@ class SmartBulb(SmartDevice):
|
|||||||
return bool(self.sys_info['is_dimmable'])
|
return bool(self.sys_info['is_dimmable'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_variable_color_temp(self):
|
def is_variable_color_temp(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Whether the bulb supports color temperature changes
|
Whether the bulb supports color temperature changes
|
||||||
|
|
||||||
@ -76,16 +79,16 @@ class SmartBulb(SmartDevice):
|
|||||||
"""
|
"""
|
||||||
return bool(self.sys_info['is_variable_color_temp'])
|
return bool(self.sys_info['is_variable_color_temp'])
|
||||||
|
|
||||||
def get_light_state(self):
|
def get_light_state(self) -> Dict:
|
||||||
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
||||||
"get_light_state")
|
"get_light_state")
|
||||||
|
|
||||||
def set_light_state(self, state):
|
def set_light_state(self, state: Dict) -> Dict:
|
||||||
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
||||||
"transition_light_state", state)
|
"transition_light_state", state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hsv(self):
|
def hsv(self) -> Optional[Tuple[int, int, int]]:
|
||||||
"""
|
"""
|
||||||
Returns the current HSV state of the bulb, if supported
|
Returns the current HSV state of the bulb, if supported
|
||||||
|
|
||||||
@ -109,7 +112,7 @@ class SmartBulb(SmartDevice):
|
|||||||
return hue, saturation, value
|
return hue, saturation, value
|
||||||
|
|
||||||
@hsv.setter
|
@hsv.setter
|
||||||
def hsv(self, state):
|
def hsv(self, state: Tuple[int, int, int]):
|
||||||
"""
|
"""
|
||||||
Sets new HSV, if supported
|
Sets new HSV, if supported
|
||||||
|
|
||||||
@ -124,10 +127,10 @@ class SmartBulb(SmartDevice):
|
|||||||
"brightness": int(state[2] * 100 / 255),
|
"brightness": int(state[2] * 100 / 255),
|
||||||
"color_temp": 0
|
"color_temp": 0
|
||||||
}
|
}
|
||||||
return self.set_light_state(light_state)
|
self.set_light_state(light_state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color_temp(self):
|
def color_temp(self) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Color temperature of the device, if supported
|
Color temperature of the device, if supported
|
||||||
|
|
||||||
@ -139,12 +142,12 @@ class SmartBulb(SmartDevice):
|
|||||||
|
|
||||||
light_state = self.get_light_state()
|
light_state = self.get_light_state()
|
||||||
if not self.is_on:
|
if not self.is_on:
|
||||||
return light_state['dft_on_state']['color_temp']
|
return int(light_state['dft_on_state']['color_temp'])
|
||||||
else:
|
else:
|
||||||
return light_state['color_temp']
|
return int(light_state['color_temp'])
|
||||||
|
|
||||||
@color_temp.setter
|
@color_temp.setter
|
||||||
def color_temp(self, temp):
|
def color_temp(self, temp: int) -> None:
|
||||||
"""
|
"""
|
||||||
Set the color temperature of the device, if supported
|
Set the color temperature of the device, if supported
|
||||||
|
|
||||||
@ -156,10 +159,10 @@ class SmartBulb(SmartDevice):
|
|||||||
light_state = {
|
light_state = {
|
||||||
"color_temp": temp,
|
"color_temp": temp,
|
||||||
}
|
}
|
||||||
return self.set_light_state(light_state)
|
self.set_light_state(light_state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Current brightness of the device, if supported
|
Current brightness of the device, if supported
|
||||||
|
|
||||||
@ -171,12 +174,12 @@ class SmartBulb(SmartDevice):
|
|||||||
|
|
||||||
light_state = self.get_light_state()
|
light_state = self.get_light_state()
|
||||||
if not self.is_on:
|
if not self.is_on:
|
||||||
return light_state['dft_on_state']['brightness']
|
return int(light_state['dft_on_state']['brightness'])
|
||||||
else:
|
else:
|
||||||
return light_state['brightness']
|
return int(light_state['brightness'])
|
||||||
|
|
||||||
@brightness.setter
|
@brightness.setter
|
||||||
def brightness(self, brightness):
|
def brightness(self, brightness: int) -> None:
|
||||||
"""
|
"""
|
||||||
Set the current brightness of the device, if supported
|
Set the current brightness of the device, if supported
|
||||||
|
|
||||||
@ -188,10 +191,10 @@ class SmartBulb(SmartDevice):
|
|||||||
light_state = {
|
light_state = {
|
||||||
"brightness": brightness,
|
"brightness": brightness,
|
||||||
}
|
}
|
||||||
return self.set_light_state(light_state)
|
self.set_light_state(light_state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve the bulb state
|
Retrieve the bulb state
|
||||||
|
|
||||||
@ -205,42 +208,8 @@ class SmartBulb(SmartDevice):
|
|||||||
return self.BULB_STATE_ON
|
return self.BULB_STATE_ON
|
||||||
return self.BULB_STATE_OFF
|
return self.BULB_STATE_OFF
|
||||||
|
|
||||||
@property
|
|
||||||
def state_information(self):
|
|
||||||
"""
|
|
||||||
Return bulb-specific state information.
|
|
||||||
:return: Bulb information dict, keys in user-presentable form.
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
info = {
|
|
||||||
'Brightness': self.brightness,
|
|
||||||
'Is dimmable': self.is_dimmable,
|
|
||||||
}
|
|
||||||
if self.is_variable_color_temp:
|
|
||||||
info["Color temperature"] = self.color_temp
|
|
||||||
if self.is_color:
|
|
||||||
info["HSV"] = self.hsv
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self):
|
|
||||||
return self.state == self.BULB_STATE_ON
|
|
||||||
|
|
||||||
def turn_off(self):
|
|
||||||
"""
|
|
||||||
Turn the bulb off.
|
|
||||||
"""
|
|
||||||
self.state = self.BULB_STATE_OFF
|
|
||||||
|
|
||||||
def turn_on(self):
|
|
||||||
"""
|
|
||||||
Turn the bulb on.
|
|
||||||
"""
|
|
||||||
self.state = self.BULB_STATE_ON
|
|
||||||
|
|
||||||
@state.setter
|
@state.setter
|
||||||
def state(self, bulb_state):
|
def state(self, bulb_state: str) -> None:
|
||||||
"""
|
"""
|
||||||
Set the new bulb state
|
Set the new bulb state
|
||||||
|
|
||||||
@ -249,17 +218,51 @@ class SmartBulb(SmartDevice):
|
|||||||
BULB_STATE_OFF
|
BULB_STATE_OFF
|
||||||
"""
|
"""
|
||||||
if bulb_state == self.BULB_STATE_ON:
|
if bulb_state == self.BULB_STATE_ON:
|
||||||
bulb_state = 1
|
new_state = 1
|
||||||
elif bulb_state == self.BULB_STATE_OFF:
|
elif bulb_state == self.BULB_STATE_OFF:
|
||||||
bulb_state = 0
|
new_state = 0
|
||||||
else:
|
else:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
light_state = {
|
light_state = {
|
||||||
"on_off": bulb_state,
|
"on_off": new_state,
|
||||||
}
|
}
|
||||||
return self.set_light_state(light_state)
|
self.set_light_state(light_state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_emeter(self):
|
def state_information(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return bulb-specific state information.
|
||||||
|
:return: Bulb information dict, keys in user-presentable form.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
info = {
|
||||||
|
'Brightness': self.brightness,
|
||||||
|
'Is dimmable': self.is_dimmable,
|
||||||
|
} # type: Dict[str, Any]
|
||||||
|
if self.is_variable_color_temp:
|
||||||
|
info["Color temperature"] = self.color_temp
|
||||||
|
if self.is_color:
|
||||||
|
info["HSV"] = self.hsv
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
return bool(self.state == self.BULB_STATE_ON)
|
||||||
|
|
||||||
|
def turn_off(self) -> None:
|
||||||
|
"""
|
||||||
|
Turn the bulb off.
|
||||||
|
"""
|
||||||
|
self.state = self.BULB_STATE_OFF
|
||||||
|
|
||||||
|
def turn_on(self) -> None:
|
||||||
|
"""
|
||||||
|
Turn the bulb on.
|
||||||
|
"""
|
||||||
|
self.state = self.BULB_STATE_ON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_emeter(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
@ -18,6 +18,7 @@ import logging
|
|||||||
import socket
|
import socket
|
||||||
import warnings
|
import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import Any, Dict, List, Tuple, Optional
|
||||||
|
|
||||||
from .types import SmartDeviceException
|
from .types import SmartDeviceException
|
||||||
from .protocol import TPLinkSmartHomeProtocol
|
from .protocol import TPLinkSmartHomeProtocol
|
||||||
@ -32,7 +33,9 @@ class SmartDevice(object):
|
|||||||
|
|
||||||
ALL_FEATURES = (FEATURE_ENERGY_METER, FEATURE_TIMER)
|
ALL_FEATURES = (FEATURE_ENERGY_METER, FEATURE_TIMER)
|
||||||
|
|
||||||
def __init__(self, ip_address, protocol=None):
|
def __init__(self,
|
||||||
|
ip_address: str,
|
||||||
|
protocol: Optional[TPLinkSmartHomeProtocol] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Create a new SmartDevice instance, identified through its IP address.
|
Create a new SmartDevice instance, identified through its IP address.
|
||||||
|
|
||||||
@ -43,8 +46,13 @@ class SmartDevice(object):
|
|||||||
if not protocol:
|
if not protocol:
|
||||||
protocol = TPLinkSmartHomeProtocol()
|
protocol = TPLinkSmartHomeProtocol()
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
|
self.emeter_type = "emeter" # type: str
|
||||||
|
self.emeter_units = False
|
||||||
|
|
||||||
def _query_helper(self, target, cmd, arg=None):
|
def _query_helper(self,
|
||||||
|
target: str,
|
||||||
|
cmd: str,
|
||||||
|
arg: Optional[Dict] = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Helper returning unwrapped result object and doing error handling.
|
Helper returning unwrapped result object and doing error handling.
|
||||||
|
|
||||||
@ -80,7 +88,7 @@ class SmartDevice(object):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self):
|
def features(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Returns features of the devices
|
Returns features of the devices
|
||||||
|
|
||||||
@ -95,7 +103,7 @@ class SmartDevice(object):
|
|||||||
)
|
)
|
||||||
warnings.simplefilter('default', DeprecationWarning)
|
warnings.simplefilter('default', DeprecationWarning)
|
||||||
if "feature" not in self.sys_info:
|
if "feature" not in self.sys_info:
|
||||||
return None
|
return []
|
||||||
|
|
||||||
features = self.sys_info['feature'].split(':')
|
features = self.sys_info['feature'].split(':')
|
||||||
|
|
||||||
@ -107,7 +115,7 @@ class SmartDevice(object):
|
|||||||
return features
|
return features
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_emeter(self):
|
def has_emeter(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks feature list for energey meter support.
|
Checks feature list for energey meter support.
|
||||||
|
|
||||||
@ -117,7 +125,7 @@ class SmartDevice(object):
|
|||||||
return SmartDevice.FEATURE_ENERGY_METER in self.features
|
return SmartDevice.FEATURE_ENERGY_METER in self.features
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sys_info(self):
|
def sys_info(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns the complete system information from the device.
|
Returns the complete system information from the device.
|
||||||
|
|
||||||
@ -126,7 +134,7 @@ class SmartDevice(object):
|
|||||||
"""
|
"""
|
||||||
return defaultdict(lambda: None, self.get_sysinfo())
|
return defaultdict(lambda: None, self.get_sysinfo())
|
||||||
|
|
||||||
def get_sysinfo(self):
|
def get_sysinfo(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Retrieve system information.
|
Retrieve system information.
|
||||||
|
|
||||||
@ -136,7 +144,7 @@ class SmartDevice(object):
|
|||||||
"""
|
"""
|
||||||
return self._query_helper("system", "get_sysinfo")
|
return self._query_helper("system", "get_sysinfo")
|
||||||
|
|
||||||
def identify(self):
|
def identify(self) -> Tuple[str, str, Any]:
|
||||||
"""
|
"""
|
||||||
Query device information to identify model and featureset
|
Query device information to identify model and featureset
|
||||||
|
|
||||||
@ -157,7 +165,7 @@ class SmartDevice(object):
|
|||||||
return info["alias"], info["model"], self.features
|
return info["alias"], info["model"], self.features
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self) -> str:
|
||||||
"""
|
"""
|
||||||
Get model of the device
|
Get model of the device
|
||||||
|
|
||||||
@ -165,20 +173,20 @@ class SmartDevice(object):
|
|||||||
:rtype: str
|
:rtype: str
|
||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
return self.sys_info['model']
|
return str(self.sys_info['model'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alias(self):
|
def alias(self) -> str:
|
||||||
"""
|
"""
|
||||||
Get current device alias (name)
|
Get current device alias (name)
|
||||||
|
|
||||||
:return: Device name aka alias.
|
:return: Device name aka alias.
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
return self.sys_info['alias']
|
return str(self.sys_info['alias'])
|
||||||
|
|
||||||
@alias.setter
|
@alias.setter
|
||||||
def alias(self, alias):
|
def alias(self, alias: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the device name aka alias.
|
Sets the device name aka alias.
|
||||||
|
|
||||||
@ -188,7 +196,7 @@ class SmartDevice(object):
|
|||||||
self._query_helper("system", "set_dev_alias", {"alias": alias})
|
self._query_helper("system", "set_dev_alias", {"alias": alias})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Returns device icon
|
Returns device icon
|
||||||
|
|
||||||
@ -201,7 +209,7 @@ class SmartDevice(object):
|
|||||||
return self._query_helper("system", "get_dev_icon")
|
return self._query_helper("system", "get_dev_icon")
|
||||||
|
|
||||||
@icon.setter
|
@icon.setter
|
||||||
def icon(self, icon):
|
def icon(self, icon: str) -> None:
|
||||||
"""
|
"""
|
||||||
Content for hash and icon are unknown.
|
Content for hash and icon are unknown.
|
||||||
|
|
||||||
@ -216,7 +224,7 @@ class SmartDevice(object):
|
|||||||
# self.initialize()
|
# self.initialize()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time(self):
|
def time(self) -> Optional[datetime.datetime]:
|
||||||
"""
|
"""
|
||||||
Returns current time from the device.
|
Returns current time from the device.
|
||||||
|
|
||||||
@ -232,7 +240,7 @@ class SmartDevice(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@time.setter
|
@time.setter
|
||||||
def time(self, ts):
|
def time(self, ts: datetime.datetime) -> None:
|
||||||
"""
|
"""
|
||||||
Sets time based on datetime object.
|
Sets time based on datetime object.
|
||||||
Note: this calls set_timezone() for setting.
|
Note: this calls set_timezone() for setting.
|
||||||
@ -266,7 +274,7 @@ class SmartDevice(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self):
|
def timezone(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Returns timezone information
|
Returns timezone information
|
||||||
|
|
||||||
@ -277,7 +285,7 @@ class SmartDevice(object):
|
|||||||
return self._query_helper("time", "get_timezone")
|
return self._query_helper("time", "get_timezone")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hw_info(self):
|
def hw_info(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Returns information about hardware
|
Returns information about hardware
|
||||||
|
|
||||||
@ -290,7 +298,7 @@ class SmartDevice(object):
|
|||||||
return {key: info[key] for key in keys if key in info}
|
return {key: info[key] for key in keys if key in info}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self):
|
def location(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Location of the device, as read from sysinfo
|
Location of the device, as read from sysinfo
|
||||||
|
|
||||||
@ -313,17 +321,19 @@ class SmartDevice(object):
|
|||||||
return loc
|
return loc
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rssi(self):
|
def rssi(self) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Returns WiFi signal strenth (rssi)
|
Returns WiFi signal strenth (rssi)
|
||||||
|
|
||||||
:return: rssi
|
:return: rssi
|
||||||
:rtype: int
|
:rtype: int
|
||||||
"""
|
"""
|
||||||
return self.sys_info["rssi"]
|
if "rssi" in self.sys_info:
|
||||||
|
return int(self.sys_info["rssi"])
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mac(self):
|
def mac(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns mac address
|
Returns mac address
|
||||||
|
|
||||||
@ -333,15 +343,15 @@ class SmartDevice(object):
|
|||||||
info = self.sys_info
|
info = self.sys_info
|
||||||
|
|
||||||
if 'mac' in info:
|
if 'mac' in info:
|
||||||
return info["mac"]
|
return str(info["mac"])
|
||||||
elif 'mic_mac' in info:
|
elif 'mic_mac' in info:
|
||||||
return info['mic_mac']
|
return str(info['mic_mac'])
|
||||||
else:
|
else:
|
||||||
raise SmartDeviceException("Unknown mac, please submit a bug"
|
raise SmartDeviceException("Unknown mac, please submit a bug"
|
||||||
"with sysinfo output.")
|
"with sysinfo output.")
|
||||||
|
|
||||||
@mac.setter
|
@mac.setter
|
||||||
def mac(self, mac):
|
def mac(self, mac: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sets new mac address
|
Sets new mac address
|
||||||
|
|
||||||
@ -350,7 +360,7 @@ class SmartDevice(object):
|
|||||||
"""
|
"""
|
||||||
self._query_helper("system", "set_mac_addr", {"mac": mac})
|
self._query_helper("system", "set_mac_addr", {"mac": mac})
|
||||||
|
|
||||||
def get_emeter_realtime(self):
|
def get_emeter_realtime(self) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
Retrive current energy readings from device.
|
Retrive current energy readings from device.
|
||||||
|
|
||||||
@ -364,7 +374,9 @@ class SmartDevice(object):
|
|||||||
|
|
||||||
return self._query_helper(self.emeter_type, "get_realtime")
|
return self._query_helper(self.emeter_type, "get_realtime")
|
||||||
|
|
||||||
def get_emeter_daily(self, year=None, month=None):
|
def get_emeter_daily(self,
|
||||||
|
year: int = None,
|
||||||
|
month: int = None) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
Retrieve daily statistics for a given month
|
Retrieve daily statistics for a given month
|
||||||
|
|
||||||
@ -395,7 +407,7 @@ class SmartDevice(object):
|
|||||||
return {entry['day']: entry[key]
|
return {entry['day']: entry[key]
|
||||||
for entry in response['day_list']}
|
for entry in response['day_list']}
|
||||||
|
|
||||||
def get_emeter_monthly(self, year=datetime.datetime.now().year):
|
def get_emeter_monthly(self, year=None) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
Retrieve monthly statistics for a given year.
|
Retrieve monthly statistics for a given year.
|
||||||
|
|
||||||
@ -408,6 +420,9 @@ class SmartDevice(object):
|
|||||||
if not self.has_emeter:
|
if not self.has_emeter:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if year is None:
|
||||||
|
year = datetime.datetime.now().year
|
||||||
|
|
||||||
response = self._query_helper(self.emeter_type, "get_monthstat",
|
response = self._query_helper(self.emeter_type, "get_monthstat",
|
||||||
{'year': year})
|
{'year': year})
|
||||||
|
|
||||||
@ -419,7 +434,7 @@ class SmartDevice(object):
|
|||||||
return {entry['month']: entry[key]
|
return {entry['month']: entry[key]
|
||||||
for entry in response['month_list']}
|
for entry in response['month_list']}
|
||||||
|
|
||||||
def erase_emeter_stats(self):
|
def erase_emeter_stats(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Erase energy meter statistics
|
Erase energy meter statistics
|
||||||
|
|
||||||
@ -429,7 +444,7 @@ class SmartDevice(object):
|
|||||||
:raises SmartDeviceException: on error
|
:raises SmartDeviceException: on error
|
||||||
"""
|
"""
|
||||||
if not self.has_emeter:
|
if not self.has_emeter:
|
||||||
return None
|
return False
|
||||||
|
|
||||||
self._query_helper(self.emeter_type, "erase_emeter_stat", None)
|
self._query_helper(self.emeter_type, "erase_emeter_stat", None)
|
||||||
|
|
||||||
@ -437,7 +452,7 @@ class SmartDevice(object):
|
|||||||
# succeeded when we are this far.
|
# succeeded when we are this far.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def current_consumption(self):
|
def current_consumption(self) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Get the current power consumption in Watt.
|
Get the current power consumption in Watt.
|
||||||
|
|
||||||
@ -450,18 +465,18 @@ class SmartDevice(object):
|
|||||||
|
|
||||||
response = self.get_emeter_realtime()
|
response = self.get_emeter_realtime()
|
||||||
if self.emeter_units:
|
if self.emeter_units:
|
||||||
return response['power_mw']
|
return float(response['power_mw'])
|
||||||
else:
|
else:
|
||||||
return response['power']
|
return float(response['power'])
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self) -> None:
|
||||||
"""
|
"""
|
||||||
Turns the device off.
|
Turns the device off.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Device subclass needs to implement this.")
|
raise NotImplementedError("Device subclass needs to implement this.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_off(self):
|
def is_off(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns whether device is off.
|
Returns whether device is off.
|
||||||
|
|
||||||
@ -470,14 +485,14 @@ class SmartDevice(object):
|
|||||||
"""
|
"""
|
||||||
return not self.is_on
|
return not self.is_on
|
||||||
|
|
||||||
def turn_on(self):
|
def turn_on(self) -> None:
|
||||||
"""
|
"""
|
||||||
Turns the device on.
|
Turns the device on.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Device subclass needs to implement this.")
|
raise NotImplementedError("Device subclass needs to implement this.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns whether the device is on.
|
Returns whether the device is on.
|
||||||
|
|
||||||
@ -488,7 +503,7 @@ class SmartDevice(object):
|
|||||||
raise NotImplementedError("Device subclass needs to implement this.")
|
raise NotImplementedError("Device subclass needs to implement this.")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_information(self):
|
def state_information(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns device-type specific, end-user friendly state information.
|
Returns device-type specific, end-user friendly state information.
|
||||||
:return: dict with state information.
|
:return: dict with state information.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from pyHS100 import SmartDevice
|
from pyHS100 import SmartDevice
|
||||||
|
|
||||||
@ -30,13 +31,15 @@ class SmartPlug(SmartDevice):
|
|||||||
SWITCH_STATE_OFF = 'OFF'
|
SWITCH_STATE_OFF = 'OFF'
|
||||||
SWITCH_STATE_UNKNOWN = 'UNKNOWN'
|
SWITCH_STATE_UNKNOWN = 'UNKNOWN'
|
||||||
|
|
||||||
def __init__(self, ip_address, protocol=None):
|
def __init__(self,
|
||||||
|
ip_address: str,
|
||||||
|
protocol: 'TPLinkSmartHomeProtocol' = None) -> None:
|
||||||
SmartDevice.__init__(self, ip_address, protocol)
|
SmartDevice.__init__(self, ip_address, protocol)
|
||||||
self.emeter_type = "emeter"
|
self.emeter_type = "emeter"
|
||||||
self.emeter_units = False
|
self.emeter_units = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve the switch state
|
Retrieve the switch state
|
||||||
|
|
||||||
@ -57,7 +60,7 @@ class SmartPlug(SmartDevice):
|
|||||||
return SmartPlug.SWITCH_STATE_UNKNOWN
|
return SmartPlug.SWITCH_STATE_UNKNOWN
|
||||||
|
|
||||||
@state.setter
|
@state.setter
|
||||||
def state(self, value):
|
def state(self, value: str):
|
||||||
"""
|
"""
|
||||||
Set the new switch state
|
Set the new switch state
|
||||||
|
|
||||||
@ -78,7 +81,7 @@ class SmartPlug(SmartDevice):
|
|||||||
raise ValueError("State %s is not valid.", value)
|
raise ValueError("State %s is not valid.", value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns whether device is on.
|
Returns whether device is on.
|
||||||
|
|
||||||
@ -103,7 +106,7 @@ class SmartPlug(SmartDevice):
|
|||||||
self._query_helper("system", "set_relay_state", {"state": 0})
|
self._query_helper("system", "set_relay_state", {"state": 0})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def led(self):
|
def led(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns the state of the led.
|
Returns the state of the led.
|
||||||
|
|
||||||
@ -112,15 +115,8 @@ class SmartPlug(SmartDevice):
|
|||||||
"""
|
"""
|
||||||
return bool(1 - self.sys_info["led_off"])
|
return bool(1 - self.sys_info["led_off"])
|
||||||
|
|
||||||
@property
|
|
||||||
def state_information(self):
|
|
||||||
return {
|
|
||||||
'LED state': self.led,
|
|
||||||
'On since': self.on_since
|
|
||||||
}
|
|
||||||
|
|
||||||
@led.setter
|
@led.setter
|
||||||
def led(self, state):
|
def led(self, state: bool):
|
||||||
"""
|
"""
|
||||||
Sets the state of the led (night mode)
|
Sets the state of the led (night mode)
|
||||||
|
|
||||||
@ -130,7 +126,7 @@ class SmartPlug(SmartDevice):
|
|||||||
self._query_helper("system", "set_led_off", {"off": int(not state)})
|
self._query_helper("system", "set_led_off", {"off": int(not state)})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def on_since(self):
|
def on_since(self) -> datetime.datetime:
|
||||||
"""
|
"""
|
||||||
Returns pretty-printed on-time
|
Returns pretty-printed on-time
|
||||||
|
|
||||||
@ -139,3 +135,10 @@ class SmartPlug(SmartDevice):
|
|||||||
"""
|
"""
|
||||||
return datetime.datetime.now() - \
|
return datetime.datetime.now() - \
|
||||||
datetime.timedelta(seconds=self.sys_info["on_time"])
|
datetime.timedelta(seconds=self.sys_info["on_time"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_information(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'LED state': self.led,
|
||||||
|
'On since': self.on_since
|
||||||
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
from unittest import TestCase, skip, skipIf
|
from unittest import TestCase, skip, skipIf
|
||||||
from voluptuous import Schema, Invalid, All, Range
|
from voluptuous import Schema, Invalid, All, Range
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from typing import Any, Dict # noqa: F401
|
||||||
|
|
||||||
from .. import SmartBulb, SmartDeviceException
|
from .. import SmartBulb, SmartDeviceException
|
||||||
from .fakes import (FakeTransportProtocol,
|
from .fakes import (FakeTransportProtocol,
|
||||||
sysinfo_lb100, sysinfo_lb110,
|
sysinfo_lb100, sysinfo_lb110,
|
||||||
sysinfo_lb120, sysinfo_lb130)
|
sysinfo_lb120, sysinfo_lb130)
|
||||||
|
|
||||||
BULB_IP = '192.168.250.186'
|
BULB_IP = '192.168.250.186'
|
||||||
SKIP_STATE_TESTS = False
|
SKIP_STATE_TESTS = False
|
||||||
|
|
||||||
@ -24,7 +26,7 @@ def check_mode(x):
|
|||||||
|
|
||||||
|
|
||||||
class TestSmartBulb(TestCase):
|
class TestSmartBulb(TestCase):
|
||||||
SYSINFO = sysinfo_lb130
|
SYSINFO = sysinfo_lb130 # type: Dict[str, Any]
|
||||||
# these schemas should go to the mainlib as
|
# these schemas should go to the mainlib as
|
||||||
# they can be useful when adding support for new features/devices
|
# they can be useful when adding support for new features/devices
|
||||||
# as well as to check that faked devices are operating properly.
|
# as well as to check that faked devices are operating properly.
|
||||||
@ -172,7 +174,7 @@ class TestSmartBulb(TestCase):
|
|||||||
|
|
||||||
def test_current_consumption(self):
|
def test_current_consumption(self):
|
||||||
x = self.bulb.current_consumption()
|
x = self.bulb.current_consumption()
|
||||||
self.assertTrue(isinstance(x, int))
|
self.assertTrue(isinstance(x, float))
|
||||||
self.assertTrue(x >= 0.0)
|
self.assertTrue(x >= 0.0)
|
||||||
|
|
||||||
def test_alias(self):
|
def test_alias(self):
|
||||||
|
@ -3,6 +3,7 @@ from voluptuous import Schema, Invalid, All, Any, Range
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
from typing import Dict # noqa: F401
|
||||||
|
|
||||||
from .. import SmartPlug, SmartDeviceException
|
from .. import SmartPlug, SmartDeviceException
|
||||||
from .fakes import (FakeTransportProtocol,
|
from .fakes import (FakeTransportProtocol,
|
||||||
@ -35,7 +36,7 @@ def check_mode(x):
|
|||||||
|
|
||||||
|
|
||||||
class TestSmartPlugHS110(TestCase):
|
class TestSmartPlugHS110(TestCase):
|
||||||
SYSINFO = sysinfo_hs110
|
SYSINFO = sysinfo_hs110 # type: Dict
|
||||||
# these schemas should go to the mainlib as
|
# these schemas should go to the mainlib as
|
||||||
# they can be useful when adding support for new features/devices
|
# they can be useful when adding support for new features/devices
|
||||||
# as well as to check that faked devices are operating properly.
|
# as well as to check that faked devices are operating properly.
|
||||||
|
@ -1,15 +1,5 @@
|
|||||||
import enum
|
|
||||||
|
|
||||||
|
|
||||||
class SmartDeviceException(Exception):
|
class SmartDeviceException(Exception):
|
||||||
"""
|
"""
|
||||||
SmartDeviceException gets raised for errors reported by the plug.
|
SmartDeviceException gets raised for errors reported by the plug.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DeviceType(enum.Enum):
|
|
||||||
Unknown = -1,
|
|
||||||
Plug = 0,
|
|
||||||
Switch = 1
|
|
||||||
Bulb = 2
|
|
||||||
|
2
setup.py
2
setup.py
@ -8,7 +8,7 @@ setup(name='pyHS100',
|
|||||||
author_email='sean@gadgetreactor.com',
|
author_email='sean@gadgetreactor.com',
|
||||||
license='GPLv3',
|
license='GPLv3',
|
||||||
packages=['pyHS100'],
|
packages=['pyHS100'],
|
||||||
install_requires=['click', 'click-datetime'],
|
install_requires=['click', 'click-datetime', 'typing'],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'pyhs100=pyHS100.cli:cli',
|
'pyhs100=pyHS100.cli:cli',
|
||||||
|
8
tox.ini
8
tox.ini
@ -1,8 +1,9 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist=py34,py35,py36,flake8
|
envlist=py33,py34,py35,py36,flake8
|
||||||
|
|
||||||
[tox:travis]
|
[tox:travis]
|
||||||
2.7 = py27
|
2.7 = py27
|
||||||
|
3.3 = py34
|
||||||
3.4 = py34
|
3.4 = py34
|
||||||
3.5 = py35
|
3.5 = py35
|
||||||
3.6 = py36
|
3.6 = py36
|
||||||
@ -13,6 +14,7 @@ deps=
|
|||||||
pytest
|
pytest
|
||||||
pytest-cov
|
pytest-cov
|
||||||
voluptuous
|
voluptuous
|
||||||
|
typing
|
||||||
commands=
|
commands=
|
||||||
py.test --cov pyHS100
|
py.test --cov pyHS100
|
||||||
|
|
||||||
@ -20,5 +22,9 @@ commands=
|
|||||||
deps=flake8
|
deps=flake8
|
||||||
commands=flake8 pyHS100
|
commands=flake8 pyHS100
|
||||||
|
|
||||||
|
[testenv:typing]
|
||||||
|
deps=mypy
|
||||||
|
commands=mypy --silent-imports pyHS100
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = .git,.tox,__pycache__,pyHS100/tests/fakes.py
|
exclude = .git,.tox,__pycache__,pyHS100/tests/fakes.py
|
||||||
|
Loading…
Reference in New Issue
Block a user