python-kasa/pyHS100/smartstrip.py

398 lines
13 KiB
Python
Raw Normal View History

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