Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@ The commands are straightforward, so feel free to check `--help` for instruction
# Library usage

The property accesses use the data obtained before by awaiting `update()`.
The values are cached until the next update call.
Each method changing the state of the device will automatically update the cached state.
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.

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

Expand Down Expand Up @@ -160,6 +163,13 @@ await plug.turn_on()
```

## 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
print("Current consumption: %s" % await plug.get_emeter_realtime())
print("Per day: %s" % await plug.get_emeter_daily(year=2016, month=12))
Expand All @@ -182,13 +192,15 @@ asyncio.run(bulb.update())

if bulb.is_dimmable:
asyncio.run(bulb.set_brightness(100))
asyncio.run(bulb.update())
print(bulb.brightness)
```

### Setting the color temperature
```python
if bulb.is_variable_color_temp:
await bulb.set_color_temp(3000)
await bulb.update()
print(bulb.color_temp)
```

Expand All @@ -199,6 +211,7 @@ Hue is given in degrees (0-360) and saturation and value in percentage.
```python
if bulb.is_color:
await bulb.set_hsv(180, 100, 100) # set to cyan
await bulb.update()
print(bulb.hsv)
```

Expand Down
14 changes: 9 additions & 5 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("--month", type=click.DateTime(["%Y-%m"]), default=None, required=False)
@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."""
click.echo(click.style("== Emeter ==", bold=True))
await dev.update()
Expand All @@ -311,17 +311,21 @@ async def emeter(dev, year, month, erase):
elif month:
click.echo(f"== For month {month.month} of {month.year} ==")
emeter_status = await dev.get_emeter_daily(year=month.year, month=month.month)

else:
emeter_status = await dev.get_emeter_realtime()
click.echo("== Current State ==")
emeter_status = dev.emeter_realtime

if isinstance(emeter_status, list):
for plug in emeter_status:
index = emeter_status.index(plug) + 1
click.echo(f"Plug {index}: {plug}")
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()
Expand Down
55 changes: 28 additions & 27 deletions kasa/smartbulb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Module for bulbs."""
import re
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, Tuple, cast

from kasa.smartdevice import (
DeviceType,
Expand Down Expand Up @@ -33,6 +33,7 @@ class SmartBulb(SmartDevice):

# change state of bulb
await p.turn_on()
await p.update()
assert p.is_on
await p.turn_off()

Expand All @@ -44,13 +45,15 @@ class SmartBulb(SmartDevice):
print("we got color!")
# set the color to an HSV tuple
await p.set_hsv(180, 100, 100)
await p.update()
# get the current HSV value
print(p.hsv)

# check whether the bulb supports setting color temperature
if p.is_variable_color_temp:
# set the color temperature in Kelvin
await p.set_color_temp(3000)
await p.update()

# get the current color temperature
print(p.color_temp)
Expand All @@ -59,6 +62,7 @@ class SmartBulb(SmartDevice):
if p.is_dimmable:
# set the bulb to 50% brightness
await p.set_brightness(50)
await p.update()

# check the current brightness
print(p.brightness)
Expand All @@ -74,7 +78,6 @@ def __init__(self, host: str) -> None:
super().__init__(host=host)
self.emeter_type = "smartlife.iot.common.emeter"
self._device_type = DeviceType.Bulb
self._light_state = None

@property # type: ignore
@requires_update
Expand Down Expand Up @@ -126,27 +129,35 @@ def valid_temperature_range(self) -> Tuple[int, int]:
return temp_range
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
@requires_update
def light_state(self) -> Optional[Dict[str, Dict]]:
def light_state(self) -> Dict[str, str]:
"""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]:
"""Query the light state."""
# TODO: add warning and refer to use light.state?
return await self._query_helper(self.LIGHT_SERVICE, "get_light_state")

async def set_light_state(self, state: Dict) -> Dict:
"""Set the light state."""
light_state = await self._query_helper(
self.LIGHT_SERVICE, "transition_light_state", state
)
await self.update()
return light_state

@property # type: ignore
Expand All @@ -160,15 +171,11 @@ def hsv(self) -> Tuple[int, int, int]:
if not self.is_color:
raise SmartDeviceException("Bulb does not support color.")

light_state = self.light_state
if not self.is_on:
hue = light_state["dft_on_state"]["hue"]
saturation = light_state["dft_on_state"]["saturation"]
value = light_state["dft_on_state"]["brightness"]
else:
hue = light_state["hue"]
saturation = light_state["saturation"]
value = light_state["brightness"]
light_state = cast(dict, self.light_state)

hue = light_state["hue"]
saturation = light_state["saturation"]
value = light_state["brightness"]

return hue, saturation, value

Expand Down Expand Up @@ -222,10 +229,7 @@ def color_temp(self) -> int:
raise SmartDeviceException("Bulb does not support colortemp.")

light_state = self.light_state
if not self.is_on:
return int(light_state["dft_on_state"]["color_temp"])
else:
return int(light_state["color_temp"])
return int(light_state["color_temp"])

@requires_update
async def set_color_temp(self, temp: int) -> None:
Expand Down Expand Up @@ -258,10 +262,7 @@ def brightness(self) -> int:
raise SmartDeviceException("Bulb is not dimmable.")

light_state = self.light_state
if not self.is_on:
return int(light_state["dft_on_state"]["brightness"])
else:
return int(light_state["brightness"])
return int(light_state["brightness"])

@requires_update
async def set_brightness(self, brightness: int) -> None:
Expand Down
Loading