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:
Teemu R 2020-05-24 17:57:54 +02:00 committed by GitHub
parent 012436c494
commit 836f1701b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 201 additions and 101 deletions

View File

@ -109,10 +109,13 @@ The commands are straightforward, so feel free to check `--help` for instruction
# Library usage # Library usage
The property accesses use the data obtained before by awaiting `update()`. The property accesses use the data obtained before by awaiting `update()`.
The values are cached until the next update call. The values are cached until the next update call. In practice this means that property accesses do no I/O and are dependent, while I/O producing methods need to be awaited.
Each method changing the state of the device will automatically update the cached state.
Errors are raised as `SmartDeviceException` instances for the user to handle. Methods changing the state of the device do not invalidate the cache (i.e., there is no implicit `update()`).
You can assume that the operation has succeeded if no exception is raised.
These methods will return the device response, which can be useful for some use cases.
Errors are raised as `SmartDeviceException` instances for the library user to handle.
## Discovering devices ## Discovering devices
@ -160,6 +163,13 @@ await plug.turn_on()
``` ```
## Getting emeter status (if applicable) ## Getting emeter status (if applicable)
The `update()` call will automatically fetch the following emeter information:
* Current consumption (accessed through `emeter_realtime` property)
* Today's consumption (`emeter_today`)
* This month's consumption (`emeter_this_month`)
You can also request this information separately:
```python ```python
print("Current consumption: %s" % await plug.get_emeter_realtime()) print("Current consumption: %s" % await plug.get_emeter_realtime())
print("Per day: %s" % await plug.get_emeter_daily(year=2016, month=12)) print("Per day: %s" % await plug.get_emeter_daily(year=2016, month=12))
@ -182,6 +192,7 @@ asyncio.run(bulb.update())
if bulb.is_dimmable: if bulb.is_dimmable:
asyncio.run(bulb.set_brightness(100)) asyncio.run(bulb.set_brightness(100))
asyncio.run(bulb.update())
print(bulb.brightness) print(bulb.brightness)
``` ```
@ -189,6 +200,7 @@ if bulb.is_dimmable:
```python ```python
if bulb.is_variable_color_temp: if bulb.is_variable_color_temp:
await bulb.set_color_temp(3000) await bulb.set_color_temp(3000)
await bulb.update()
print(bulb.color_temp) print(bulb.color_temp)
``` ```
@ -199,6 +211,7 @@ Hue is given in degrees (0-360) and saturation and value in percentage.
```python ```python
if bulb.is_color: if bulb.is_color:
await bulb.set_hsv(180, 100, 100) # set to cyan await bulb.set_hsv(180, 100, 100) # set to cyan
await bulb.update()
print(bulb.hsv) print(bulb.hsv)
``` ```

View File

@ -292,7 +292,7 @@ async def raw_command(dev: SmartDevice, module, command, parameters):
@click.option("--year", type=click.DateTime(["%Y"]), default=None, required=False) @click.option("--year", type=click.DateTime(["%Y"]), default=None, required=False)
@click.option("--month", type=click.DateTime(["%Y-%m"]), default=None, required=False) @click.option("--month", type=click.DateTime(["%Y-%m"]), default=None, required=False)
@click.option("--erase", is_flag=True) @click.option("--erase", is_flag=True)
async def emeter(dev, year, month, erase): async def emeter(dev: SmartDevice, year, month, erase):
"""Query emeter for historical consumption.""" """Query emeter for historical consumption."""
click.echo(click.style("== Emeter ==", bold=True)) click.echo(click.style("== Emeter ==", bold=True))
await dev.update() await dev.update()
@ -311,17 +311,21 @@ async def emeter(dev, year, month, erase):
elif month: elif month:
click.echo(f"== For month {month.month} of {month.year} ==") click.echo(f"== For month {month.month} of {month.year} ==")
emeter_status = await dev.get_emeter_daily(year=month.year, month=month.month) emeter_status = await dev.get_emeter_daily(year=month.year, month=month.month)
else: else:
emeter_status = await dev.get_emeter_realtime() emeter_status = dev.emeter_realtime
click.echo("== Current State ==")
if isinstance(emeter_status, list): if isinstance(emeter_status, list):
for plug in emeter_status: for plug in emeter_status:
index = emeter_status.index(plug) + 1 index = emeter_status.index(plug) + 1
click.echo(f"Plug {index}: {plug}") click.echo(f"Plug {index}: {plug}")
else: else:
click.echo(str(emeter_status)) click.echo("Current: %s A" % emeter_status["current"])
click.echo("Voltage: %s V" % emeter_status["voltage"])
click.echo("Power: %s W" % emeter_status["power"])
click.echo("Total consumption: %s kWh" % emeter_status["total"])
click.echo("Today: %s kWh" % dev.emeter_today)
click.echo("This month: %s kWh" % dev.emeter_this_month)
@cli.command() @cli.command()

View File

@ -1,6 +1,6 @@
"""Module for bulbs.""" """Module for bulbs."""
import re import re
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Tuple, cast
from kasa.smartdevice import ( from kasa.smartdevice import (
DeviceType, DeviceType,
@ -33,6 +33,7 @@ class SmartBulb(SmartDevice):
# change state of bulb # change state of bulb
await p.turn_on() await p.turn_on()
await p.update()
assert p.is_on assert p.is_on
await p.turn_off() await p.turn_off()
@ -44,6 +45,7 @@ class SmartBulb(SmartDevice):
print("we got color!") print("we got color!")
# set the color to an HSV tuple # set the color to an HSV tuple
await p.set_hsv(180, 100, 100) await p.set_hsv(180, 100, 100)
await p.update()
# get the current HSV value # get the current HSV value
print(p.hsv) print(p.hsv)
@ -51,6 +53,7 @@ class SmartBulb(SmartDevice):
if p.is_variable_color_temp: if p.is_variable_color_temp:
# set the color temperature in Kelvin # set the color temperature in Kelvin
await p.set_color_temp(3000) await p.set_color_temp(3000)
await p.update()
# get the current color temperature # get the current color temperature
print(p.color_temp) print(p.color_temp)
@ -59,6 +62,7 @@ class SmartBulb(SmartDevice):
if p.is_dimmable: if p.is_dimmable:
# set the bulb to 50% brightness # set the bulb to 50% brightness
await p.set_brightness(50) await p.set_brightness(50)
await p.update()
# check the current brightness # check the current brightness
print(p.brightness) print(p.brightness)
@ -74,7 +78,6 @@ class SmartBulb(SmartDevice):
super().__init__(host=host) super().__init__(host=host)
self.emeter_type = "smartlife.iot.common.emeter" self.emeter_type = "smartlife.iot.common.emeter"
self._device_type = DeviceType.Bulb self._device_type = DeviceType.Bulb
self._light_state = None
@property # type: ignore @property # type: ignore
@requires_update @requires_update
@ -126,19 +129,28 @@ class SmartBulb(SmartDevice):
return temp_range return temp_range
return (0, 0) return (0, 0)
async def update(self):
"""Update `sys_info and `light_state`."""
self._sys_info = await self.get_sys_info()
self._light_state = await self.get_light_state()
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def light_state(self) -> Optional[Dict[str, Dict]]: def light_state(self) -> Dict[str, str]:
"""Query the light state.""" """Query the light state."""
return self._light_state light_state = self._last_update["system"]["get_sysinfo"]["light_state"]
if light_state is None:
raise SmartDeviceException(
"The device has no light_state or you have not called update()"
)
# if the bulb is off, its state is stored under a different key
# as is_on property depends on on_off itself, we check it here manually
is_on = light_state["on_off"]
if not is_on:
off_state = {**light_state["dft_on_state"], "on_off": is_on}
return cast(dict, off_state)
return light_state
async def get_light_state(self) -> Dict[str, Dict]: async def get_light_state(self) -> Dict[str, Dict]:
"""Query the light state.""" """Query the light state."""
# TODO: add warning and refer to use light.state?
return await self._query_helper(self.LIGHT_SERVICE, "get_light_state") return await self._query_helper(self.LIGHT_SERVICE, "get_light_state")
async def set_light_state(self, state: Dict) -> Dict: async def set_light_state(self, state: Dict) -> Dict:
@ -146,7 +158,6 @@ class SmartBulb(SmartDevice):
light_state = await self._query_helper( light_state = await self._query_helper(
self.LIGHT_SERVICE, "transition_light_state", state self.LIGHT_SERVICE, "transition_light_state", state
) )
await self.update()
return light_state return light_state
@property # type: ignore @property # type: ignore
@ -160,15 +171,11 @@ class SmartBulb(SmartDevice):
if not self.is_color: if not self.is_color:
raise SmartDeviceException("Bulb does not support color.") raise SmartDeviceException("Bulb does not support color.")
light_state = self.light_state light_state = cast(dict, self.light_state)
if not self.is_on:
hue = light_state["dft_on_state"]["hue"] hue = light_state["hue"]
saturation = light_state["dft_on_state"]["saturation"] saturation = light_state["saturation"]
value = light_state["dft_on_state"]["brightness"] value = light_state["brightness"]
else:
hue = light_state["hue"]
saturation = light_state["saturation"]
value = light_state["brightness"]
return hue, saturation, value return hue, saturation, value
@ -222,10 +229,7 @@ class SmartBulb(SmartDevice):
raise SmartDeviceException("Bulb does not support colortemp.") raise SmartDeviceException("Bulb does not support colortemp.")
light_state = self.light_state light_state = self.light_state
if not self.is_on: return int(light_state["color_temp"])
return int(light_state["dft_on_state"]["color_temp"])
else:
return int(light_state["color_temp"])
@requires_update @requires_update
async def set_color_temp(self, temp: int) -> None: async def set_color_temp(self, temp: int) -> None:
@ -258,10 +262,7 @@ class SmartBulb(SmartDevice):
raise SmartDeviceException("Bulb is not dimmable.") raise SmartDeviceException("Bulb is not dimmable.")
light_state = self.light_state light_state = self.light_state
if not self.is_on: return int(light_state["brightness"])
return int(light_state["dft_on_state"]["brightness"])
else:
return int(light_state["brightness"])
@requires_update @requires_update
async def set_brightness(self, brightness: int) -> None: async def set_brightness(self, brightness: int) -> None:

View File

@ -99,7 +99,10 @@ def requires_update(f):
@functools.wraps(f) @functools.wraps(f)
async def wrapped(*args, **kwargs): async def wrapped(*args, **kwargs):
self = args[0] 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) return await f(*args, **kwargs)
else: else:
@ -107,7 +110,10 @@ def requires_update(f):
@functools.wraps(f) @functools.wraps(f)
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
self = args[0] 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) return f(*args, **kwargs)
f.requires_update = True f.requires_update = True
@ -129,7 +135,20 @@ class SmartDevice:
self.emeter_type = "emeter" self.emeter_type = "emeter"
_LOGGER.debug("Initializing %s of type %s", self.host, type(self)) _LOGGER.debug("Initializing %s of type %s", self.host, type(self))
self._device_type = DeviceType.Unknown 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( async def _query_helper(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None 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 target: Target system {system, time, emeter, ..}
:param cmd: Command to execute :param cmd: Command to execute
:param arg: JSON object passed as parameter to the command :param arg: JSON object passed as parameter to the command
:param child_ids: ids of child devices
:return: Unwrapped result for the call. :return: Unwrapped result for the call.
:rtype: dict :rtype: dict
:raises SmartDeviceException: if command was not executed correctly :raises SmartDeviceException: if command was not executed correctly
""" """
request: Dict[str, Any] = {target: {cmd: arg}} request = self._create_request(target, cmd, arg, child_ids)
if child_ids is not None:
request = {"context": {"child_ids": child_ids}, target: {cmd: arg}}
try: try:
response = await self.protocol.query(host=self.host, request=request) 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`. 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 @property # type: ignore
@requires_update @requires_update
@ -207,8 +233,7 @@ class SmartDevice:
:rtype dict :rtype dict
:raises SmartDeviceException: on error :raises SmartDeviceException: on error
""" """
assert self._sys_info is not None return self._sys_info # type: ignore
return self._sys_info
@property # type: ignore @property # type: ignore
@requires_update @requires_update
@ -239,8 +264,7 @@ class SmartDevice:
:param alias: New alias (name) :param alias: New alias (name)
:raises SmartDeviceException: on error :raises SmartDeviceException: on error
""" """
await self._query_helper("system", "set_dev_alias", {"alias": alias}) return await self._query_helper("system", "set_dev_alias", {"alias": alias})
await self.update()
async def get_icon(self) -> Dict: async def get_icon(self) -> Dict:
"""Return device icon. """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 :param str mac: mac in hexadecimal with colons, e.g. 01:23:45:67:89:ab
:raises SmartDeviceException: on error :raises SmartDeviceException: on error
""" """
await self._query_helper("system", "set_mac_addr", {"mac": mac}) return await self._query_helper("system", "set_mac_addr", {"mac": mac})
await self.update()
@property # type: ignore
@requires_update @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: async def get_emeter_realtime(self) -> EmeterStatus:
"""Retrieve current energy readings. """Retrieve current energy readings.
@ -431,7 +462,83 @@ class SmartDevice:
return EmeterStatus(await self._query_helper(self.emeter_type, "get_realtime")) 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 @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( async def get_emeter_daily(
self, year: int = None, month: int = None, kwh: bool = True self, year: int = None, month: int = None, kwh: bool = True
) -> Dict: ) -> Dict:
@ -456,15 +563,8 @@ class SmartDevice:
response = await self._query_helper( response = await self._query_helper(
self.emeter_type, "get_daystat", {"month": month, "year": year} self.emeter_type, "get_daystat", {"month": month, "year": year}
) )
response = [EmeterStatus(**x) for x in response["day_list"]]
key = "energy_wh" return self._emeter_convert_emeter_data(response["day_list"], kwh)
if kwh:
key = "energy"
data = {entry["day"]: entry[key] for entry in response}
return data
@requires_update @requires_update
async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict: async def get_emeter_monthly(self, year: int = None, kwh: bool = True) -> Dict:
@ -485,13 +585,8 @@ class SmartDevice:
response = await self._query_helper( response = await self._query_helper(
self.emeter_type, "get_monthstat", {"year": year} self.emeter_type, "get_monthstat", {"year": year}
) )
response = [EmeterStatus(**x) for x in response["month_list"]]
key = "energy_wh" return self._emeter_convert_emeter_data(response["month_list"], kwh)
if kwh:
key = "energy"
return {entry["month"]: entry[key] for entry in response}
@requires_update @requires_update
async def erase_emeter_stats(self): async def erase_emeter_stats(self):
@ -503,8 +598,7 @@ class SmartDevice:
if not self.has_emeter: if not self.has_emeter:
raise SmartDeviceException("Device has no emeter") raise SmartDeviceException("Device has no emeter")
await self._query_helper(self.emeter_type, "erase_emeter_stat", None) return await self._query_helper(self.emeter_type, "erase_emeter_stat", None)
await self.update()
@requires_update @requires_update
async def current_consumption(self) -> float: async def current_consumption(self) -> float:
@ -673,11 +767,6 @@ class SmartDevice:
return False return False
def __repr__(self): def __repr__(self):
return "<{} model {} at {} ({}), is_on: {} - dev specific: {}>".format( if self._last_update is None:
self.__class__.__name__, return f"<{self._device_type} at {self.host} - update() needed>"
self.model, return f"<{self._device_type} model {self.model} at {self.host} ({self.alias}), is_on: {self.is_on} - dev specific: {self.state_information}>"
self.host,
self.alias,
self.is_on,
self.state_information,
)

View File

@ -60,10 +60,9 @@ class SmartDimmer(SmartPlug):
if not isinstance(value, int): if not isinstance(value, int):
raise ValueError("Brightness must be integer, " "not of %s.", type(value)) raise ValueError("Brightness must be integer, " "not of %s.", type(value))
elif 0 <= value <= 100: elif 0 <= value <= 100:
await self._query_helper( return await self._query_helper(
"smartlife.iot.dimmer", "set_brightness", {"brightness": value} "smartlife.iot.dimmer", "set_brightness", {"brightness": value}
) )
await self.update()
else: else:
raise ValueError("Brightness value %s is not valid." % value) raise ValueError("Brightness value %s is not valid." % value)

View File

@ -50,16 +50,14 @@ class SmartPlug(SmartDevice):
:raises SmartDeviceException: on error :raises SmartDeviceException: on error
""" """
await self._query_helper("system", "set_relay_state", {"state": 1}) return await self._query_helper("system", "set_relay_state", {"state": 1})
await self.update()
async def turn_off(self): async def turn_off(self):
"""Turn the switch off. """Turn the switch off.
:raises SmartDeviceException: on error :raises SmartDeviceException: on error
""" """
await self._query_helper("system", "set_relay_state", {"state": 0}) return await self._query_helper("system", "set_relay_state", {"state": 0})
await self.update()
@property # type: ignore @property # type: ignore
@requires_update @requires_update
@ -78,8 +76,9 @@ class SmartPlug(SmartDevice):
:param bool state: True to set led on, False to set led off :param bool state: True to set led on, False to set led off
:raises SmartDeviceException: on error :raises SmartDeviceException: on error
""" """
await self._query_helper("system", "set_led_off", {"off": int(not state)}) return await self._query_helper(
await self.update() "system", "set_led_off", {"off": int(not state)}
)
@property # type: ignore @property # type: ignore
@requires_update @requires_update

View File

@ -146,12 +146,11 @@ class SmartStrip(SmartDevice):
:return: Strip information dict, keys in user-presentable form. :return: Strip information dict, keys in user-presentable form.
:rtype: dict :rtype: dict
""" """
state: Dict[str, Any] = {"LED state": self.led} return {
for plug in self.plugs: "LED state": self.led,
if plug.is_on: "Childs count": len(self.plugs),
state["Plug %s on since" % str(plug)] = self.on_since "On since": self.on_since,
}
return state
async def current_consumption(self) -> float: async def current_consumption(self) -> float:
"""Get the current power consumption in watts. """Get the current power consumption in watts.
@ -218,6 +217,7 @@ class SmartStrip(SmartDevice):
plug_emeter_monthly = await plug.get_emeter_monthly(year=year, kwh=kwh) plug_emeter_monthly = await plug.get_emeter_monthly(year=year, kwh=kwh)
for month, value in plug_emeter_monthly: for month, value in plug_emeter_monthly:
emeter_monthly[month] += value emeter_monthly[month] += value
return emeter_monthly return emeter_monthly
@requires_update @requires_update
@ -245,7 +245,8 @@ class SmartStripPlug(SmartPlug):
self.parent = parent self.parent = parent
self.child_id = child_id self.child_id = child_id
self._sys_info = {**self.parent.sys_info, **self._get_child_info()} self._last_update = parent._last_update
self._sys_info = parent._sys_info
async def update(self): async def update(self):
"""Override the update to no-op and inform the user.""" """Override the update to no-op and inform the user."""

View File

@ -313,9 +313,7 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
def transition_light_state(self, x, *args): def transition_light_state(self, x, *args):
_LOGGER.debug("Setting light state to %s", x) _LOGGER.debug("Setting light state to %s", x)
light_state = self.proto["smartlife.iot.smartbulb.lightingservice"][ light_state = self.proto["system"]["get_sysinfo"]["light_state"]
"get_light_state"
]
# The required change depends on the light state, # The required change depends on the light state,
# exception being turning the bulb on and off # exception being turning the bulb on and off
@ -323,15 +321,11 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
if x["on_off"] and not light_state["on_off"]: # turning on if x["on_off"] and not light_state["on_off"]: # turning on
new_state = light_state["dft_on_state"] new_state = light_state["dft_on_state"]
new_state["on_off"] = 1 new_state["on_off"] = 1
self.proto["smartlife.iot.smartbulb.lightingservice"][ self.proto["system"]["get_sysinfo"]["light_state"] = new_state
"get_light_state"
] = new_state
elif not x["on_off"] and light_state["on_off"]: elif not x["on_off"] and light_state["on_off"]:
new_state = {"dft_on_state": light_state, "on_off": 0} new_state = {"dft_on_state": light_state, "on_off": 0}
self.proto["smartlife.iot.smartbulb.lightingservice"][ self.proto["system"]["get_sysinfo"]["light_state"] = new_state
"get_light_state"
] = new_state
return return
@ -343,11 +337,9 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
light_state[key] = x[key] light_state[key] = x[key]
def light_state(self, x, *args): def light_state(self, x, *args):
light_state = self.proto["smartlife.iot.smartbulb.lightingservice"][ light_state = self.proto["system"]["get_sysinfo"]["light_state"]
"get_light_state"
]
# Our tests have light state off, so we simply return the dft_on_state when device is on. # Our tests have light state off, so we simply return the dft_on_state when device is on.
_LOGGER.info("reporting light state: %s", light_state) _LOGGER.debug("reporting light state: %s", light_state)
if light_state["on_off"]: if light_state["on_off"]:
return light_state["dft_on_state"] return light_state["dft_on_state"]
else: else:

View File

@ -66,7 +66,7 @@ async def test_emeter(dev: SmartDevice, mocker):
assert "Device has no emeter" in res.output assert "Device has no emeter" in res.output
return return
assert "Current State" in res.output assert "== Emeter ==" in res.output
monthly = mocker.patch.object(dev, "get_emeter_monthly") monthly = mocker.patch.object(dev, "get_emeter_monthly")
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev) res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)

View File

@ -419,9 +419,11 @@ async def test_children_alias(dev):
for plug in dev.plugs: for plug in dev.plugs:
original = plug.alias original = plug.alias
await plug.set_alias(alias=test_alias) await plug.set_alias(alias=test_alias)
await dev.update() # TODO: set_alias does not call parent's update()..
assert plug.alias == test_alias assert plug.alias == test_alias
await plug.set_alias(alias=original) await plug.set_alias(alias=original)
await dev.update() # TODO: set_alias does not call parent's update()..
assert plug.alias == original assert plug.alias == original