mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-10-13 10:58:02 +00:00
Optimize I/O access (#59)
* Optimize I/O access A single update() will now fetch information from all interesting modules, including the current device state and the emeter information. In practice, this will allow dropping the number of I/O reqs per homeassistant update cycle to 1, which is paramount at least for bulbs which are very picky about sequential accesses. This can be further extend to other modules/methods, if needed. Currently fetched data: * sysinfo * realtime, today's and this months emeter stats New properties: * emeter_realtime: return the most recent emeter information, update()-version of get_emeter_realtime() * emeter_today: returning today's energy consumption * emeter_this_month: same for this month Other changes: * Accessing @requires_update properties will cause SmartDeviceException if the device has not ever been update()d * Fix __repr__ for devices that haven't been updated * Smartbulb uses now the state data from get_sysinfo instead of separately querying the bulb service * SmartStrip's state_information no longer lists onsince for separate plugs * The above mentioned properties are now printed out by cli * Simplify is_on handling for bulbs * remove implicit updates, return device responses for actions, update README.md instructions. fixes #61
This commit is contained in:
@@ -99,7 +99,10 @@ def requires_update(f):
|
||||
@functools.wraps(f)
|
||||
async def wrapped(*args, **kwargs):
|
||||
self = args[0]
|
||||
assert self._sys_info is not None
|
||||
if self._last_update is None:
|
||||
raise SmartDeviceException(
|
||||
"You need to await update() to access the data"
|
||||
)
|
||||
return await f(*args, **kwargs)
|
||||
|
||||
else:
|
||||
@@ -107,7 +110,10 @@ def requires_update(f):
|
||||
@functools.wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
self = args[0]
|
||||
assert self._sys_info is not None
|
||||
if self._last_update is None:
|
||||
raise SmartDeviceException(
|
||||
"You need to await update() to access the data"
|
||||
)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
f.requires_update = True
|
||||
@@ -129,7 +135,20 @@ class SmartDevice:
|
||||
self.emeter_type = "emeter"
|
||||
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
|
||||
self._device_type = DeviceType.Unknown
|
||||
self._sys_info: Optional[Dict] = None
|
||||
# TODO: typing Any is just as using Optional[Dict] would require separate checks in
|
||||
# accessors. the @updated_required decorator does not ensure mypy that these
|
||||
# are not accessed incorrectly.
|
||||
self._last_update: Any = None
|
||||
self._sys_info: Any = None # TODO: this is here to avoid changing tests
|
||||
|
||||
def _create_request(
|
||||
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
|
||||
):
|
||||
request: Dict[str, Any] = {target: {cmd: arg}}
|
||||
if child_ids is not None:
|
||||
request = {"context": {"child_ids": child_ids}, target: {cmd: arg}}
|
||||
|
||||
return request
|
||||
|
||||
async def _query_helper(
|
||||
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
|
||||
@@ -139,13 +158,12 @@ class SmartDevice:
|
||||
:param target: Target system {system, time, emeter, ..}
|
||||
:param cmd: Command to execute
|
||||
:param arg: JSON object passed as parameter to the command
|
||||
:param child_ids: ids of child devices
|
||||
:return: Unwrapped result for the call.
|
||||
:rtype: dict
|
||||
:raises SmartDeviceException: if command was not executed correctly
|
||||
"""
|
||||
request: Dict[str, Any] = {target: {cmd: arg}}
|
||||
if child_ids is not None:
|
||||
request = {"context": {"child_ids": child_ids}, target: {cmd: arg}}
|
||||
request = self._create_request(target, cmd, arg, child_ids)
|
||||
|
||||
try:
|
||||
response = await self.protocol.query(host=self.host, request=request)
|
||||
@@ -196,7 +214,15 @@ class SmartDevice:
|
||||
|
||||
Needed for methods that are decorated with `requires_update`.
|
||||
"""
|
||||
self._sys_info = await self.get_sys_info()
|
||||
req = {}
|
||||
req.update(self._create_request("system", "get_sysinfo"))
|
||||
|
||||
# Check for emeter if we were never updated, or if the device has emeter
|
||||
if self._last_update is None or self.has_emeter:
|
||||
req.update(self._create_emeter_request())
|
||||
self._last_update = await self.protocol.query(self.host, req)
|
||||
# TODO: keep accessible for tests
|
||||
self._sys_info = self._last_update["system"]["get_sysinfo"]
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@@ -207,8 +233,7 @@ class SmartDevice:
|
||||
:rtype dict
|
||||
:raises SmartDeviceException: on error
|
||||
"""
|
||||
assert self._sys_info is not None
|
||||
return self._sys_info
|
||||
return self._sys_info # type: ignore
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
@@ -239,8 +264,7 @@ class SmartDevice:
|
||||
:param alias: New alias (name)
|
||||
:raises SmartDeviceException: on error
|
||||
"""
|
||||
await self._query_helper("system", "set_dev_alias", {"alias": alias})
|
||||
await self.update()
|
||||
return await self._query_helper("system", "set_dev_alias", {"alias": alias})
|
||||
|
||||
async def get_icon(self) -> Dict:
|
||||
"""Return device icon.
|
||||
@@ -415,10 +439,17 @@ class SmartDevice:
|
||||
:param str mac: mac in hexadecimal with colons, e.g. 01:23:45:67:89:ab
|
||||
:raises SmartDeviceException: on error
|
||||
"""
|
||||
await self._query_helper("system", "set_mac_addr", {"mac": mac})
|
||||
await self.update()
|
||||
return await self._query_helper("system", "set_mac_addr", {"mac": mac})
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def emeter_realtime(self) -> EmeterStatus:
|
||||
"""Return current emeter status."""
|
||||
if not self.has_emeter:
|
||||
raise SmartDeviceException("Device has no emeter")
|
||||
|
||||
return EmeterStatus(self._last_update[self.emeter_type]["get_realtime"])
|
||||
|
||||
async def get_emeter_realtime(self) -> EmeterStatus:
|
||||
"""Retrieve current energy readings.
|
||||
|
||||
@@ -431,7 +462,83 @@ class SmartDevice:
|
||||
|
||||
return EmeterStatus(await self._query_helper(self.emeter_type, "get_realtime"))
|
||||
|
||||
def _create_emeter_request(self, year: int = None, month: int = None):
|
||||
"""Create a Internal method for building a request for all emeter statistics at once."""
|
||||
if year is None:
|
||||
year = datetime.now().year
|
||||
if month is None:
|
||||
month = datetime.now().month
|
||||
|
||||
import collections.abc
|
||||
|
||||
def update(d, u):
|
||||
"""Update dict recursively."""
|
||||
for k, v in u.items():
|
||||
if isinstance(v, collections.abc.Mapping):
|
||||
d[k] = update(d.get(k, {}), v)
|
||||
else:
|
||||
d[k] = v
|
||||
return d
|
||||
|
||||
req: Dict[str, Any] = {}
|
||||
update(req, self._create_request(self.emeter_type, "get_realtime"))
|
||||
update(
|
||||
req, self._create_request(self.emeter_type, "get_monthstat", {"year": year})
|
||||
)
|
||||
update(
|
||||
req,
|
||||
self._create_request(
|
||||
self.emeter_type, "get_daystat", {"month": month, "year": year}
|
||||
),
|
||||
)
|
||||
|
||||
return req
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def emeter_today(self) -> Optional[float]:
|
||||
"""Return today's energy consumption in kWh."""
|
||||
raw_data = self._last_update[self.emeter_type]["get_daystat"]["day_list"]
|
||||
data = self._emeter_convert_emeter_data(raw_data)
|
||||
today = datetime.now().day
|
||||
|
||||
if today in data:
|
||||
return data[today]
|
||||
|
||||
return None
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def emeter_this_month(self) -> Optional[float]:
|
||||
"""Return this month's energy consumption in kWh."""
|
||||
raw_data = self._last_update[self.emeter_type]["get_monthstat"]["month_list"]
|
||||
data = self._emeter_convert_emeter_data(raw_data)
|
||||
current_month = datetime.now().month
|
||||
|
||||
if current_month in data:
|
||||
return data[current_month]
|
||||
|
||||
return None
|
||||
|
||||
def _emeter_convert_emeter_data(self, data, kwh=True) -> Dict:
|
||||
"""Return emeter information keyed with the day/month.."""
|
||||
response = [EmeterStatus(**x) for x in data]
|
||||
|
||||
if not response:
|
||||
return {}
|
||||
|
||||
energy_key = "energy_wh"
|
||||
if kwh:
|
||||
energy_key = "energy"
|
||||
|
||||
entry_key = "month"
|
||||
if "day" in response[0]:
|
||||
entry_key = "day"
|
||||
|
||||
data = {entry[entry_key]: entry[energy_key] for entry in response}
|
||||
|
||||
return data
|
||||
|
||||
async def get_emeter_daily(
|
||||
self, year: int = None, month: int = None, kwh: bool = True
|
||||
) -> Dict:
|
||||
@@ -456,15 +563,8 @@ class SmartDevice:
|
||||
response = await self._query_helper(
|
||||
self.emeter_type, "get_daystat", {"month": month, "year": year}
|
||||
)
|
||||
response = [EmeterStatus(**x) for x in response["day_list"]]
|
||||
|
||||
key = "energy_wh"
|
||||
if kwh:
|
||||
key = "energy"
|
||||
|
||||
data = {entry["day"]: entry[key] for entry in response}
|
||||
|
||||
return data
|
||||
return self._emeter_convert_emeter_data(response["day_list"], kwh)
|
||||
|
||||
@requires_update
|
||||
async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict:
|
||||
@@ -485,13 +585,8 @@ class SmartDevice:
|
||||
response = await self._query_helper(
|
||||
self.emeter_type, "get_monthstat", {"year": year}
|
||||
)
|
||||
response = [EmeterStatus(**x) for x in response["month_list"]]
|
||||
|
||||
key = "energy_wh"
|
||||
if kwh:
|
||||
key = "energy"
|
||||
|
||||
return {entry["month"]: entry[key] for entry in response}
|
||||
return self._emeter_convert_emeter_data(response["month_list"], kwh)
|
||||
|
||||
@requires_update
|
||||
async def erase_emeter_stats(self):
|
||||
@@ -503,8 +598,7 @@ class SmartDevice:
|
||||
if not self.has_emeter:
|
||||
raise SmartDeviceException("Device has no emeter")
|
||||
|
||||
await self._query_helper(self.emeter_type, "erase_emeter_stat", None)
|
||||
await self.update()
|
||||
return await self._query_helper(self.emeter_type, "erase_emeter_stat", None)
|
||||
|
||||
@requires_update
|
||||
async def current_consumption(self) -> float:
|
||||
@@ -673,11 +767,6 @@ class SmartDevice:
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return "<{} model {} at {} ({}), is_on: {} - dev specific: {}>".format(
|
||||
self.__class__.__name__,
|
||||
self.model,
|
||||
self.host,
|
||||
self.alias,
|
||||
self.is_on,
|
||||
self.state_information,
|
||||
)
|
||||
if self._last_update is None:
|
||||
return f"<{self._device_type} at {self.host} - update() needed>"
|
||||
return f"<{self._device_type} model {self.model} at {self.host} ({self.alias}), is_on: {self.is_on} - dev specific: {self.state_information}>"
|
||||
|
Reference in New Issue
Block a user