Skip to content
Merged
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,18 @@ or the `parse_pcap.py` script contained inside the `devtools` directory.
* HS110

### Power Strips

* HS300
* KP303

### Wall switches

* HS200
* HS210
* HS220

### Bulbs

* LB100
* LB110
* LB120
Expand All @@ -131,6 +134,10 @@ or the `parse_pcap.py` script contained inside the `devtools` directory.
* KL120
* KL130

### Light strips

* KL430

**Contributions (be it adding missing features, fixing bugs or improving documentation) are more than welcome, feel free to submit pull requests!**

### Resources
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ python-kasa documentation
smartplug
smartdimmer
smartstrip
smartlightstrip
6 changes: 6 additions & 0 deletions docs/source/smartlightstrip.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Light strips
============

.. autoclass:: kasa.SmartLightStrip
:members:
:undoc-members:
2 changes: 2 additions & 0 deletions kasa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from kasa.smartbulb import SmartBulb
from kasa.smartdevice import DeviceType, EmeterStatus, SmartDevice
from kasa.smartdimmer import SmartDimmer
from kasa.smartlightstrip import SmartLightStrip
from kasa.smartplug import SmartPlug
from kasa.smartstrip import SmartStrip

Expand All @@ -35,4 +36,5 @@
"SmartPlug",
"SmartStrip",
"SmartDimmer",
"SmartLightStrip",
]
16 changes: 13 additions & 3 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@

import asyncclick as click

from kasa import Discover, SmartBulb, SmartDevice, SmartPlug, SmartStrip
from kasa import (
Discover,
SmartBulb,
SmartDevice,
SmartLightStrip,
SmartPlug,
SmartStrip,
)

click.anyio_backend = "asyncio"

Expand Down Expand Up @@ -37,10 +44,11 @@
@click.option("-d", "--debug", default=False, is_flag=True)
@click.option("--bulb", default=False, is_flag=True)
@click.option("--plug", default=False, is_flag=True)
@click.option("--lightstrip", default=False, is_flag=True)
@click.option("--strip", default=False, is_flag=True)
@click.version_option()
@click.pass_context
async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
async def cli(ctx, host, alias, target, debug, bulb, plug, lightstrip, strip):
"""A tool for controlling TP-Link smart home devices.""" # noqa
if debug:
logging.basicConfig(level=logging.DEBUG)
Expand All @@ -64,7 +72,7 @@ async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
await ctx.invoke(discover)
return
else:
if not bulb and not plug and not strip:
if not bulb and not plug and not strip and not lightstrip:
click.echo("No --strip nor --bulb nor --plug given, discovering..")
dev = await Discover.discover_single(host)
elif bulb:
Expand All @@ -73,6 +81,8 @@ async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
dev = SmartPlug(host)
elif strip:
dev = SmartStrip(host)
elif lightstrip:
dev = SmartLightStrip(host)
else:
click.echo("Unable to detect type, use --strip or --bulb or --plug!")
return
Expand Down
9 changes: 9 additions & 0 deletions kasa/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from kasa.smartbulb import SmartBulb
from kasa.smartdevice import SmartDevice, SmartDeviceException
from kasa.smartdimmer import SmartDimmer
from kasa.smartlightstrip import SmartLightStrip
from kasa.smartplug import SmartPlug
from kasa.smartstrip import SmartStrip

Expand Down Expand Up @@ -227,11 +228,19 @@ def _get_device_class(info: dict) -> Type[SmartDevice]:
and "get_dimmer_parameters" in info["smartlife.iot.dimmer"]
):
return SmartDimmer

elif "smartplug" in type_.lower() and "children" in sysinfo:
return SmartStrip

elif "smartplug" in type_.lower():
if "children" in sysinfo:
return SmartStrip

return SmartPlug
elif "smartbulb" in type_.lower():
if "length" in sysinfo: # strips have length
return SmartLightStrip

return SmartBulb

raise SmartDeviceException("Unknown device type: %s", type_)
Expand Down
4 changes: 3 additions & 1 deletion kasa/smartbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"KL130": (2500, 9000),
r"KL120\(EU\)": (2700, 6500),
r"KL120\(US\)": (2700, 5000),
r"KL430\(US\)": (2500, 9000),
}


Expand Down Expand Up @@ -89,6 +90,7 @@ class SmartBulb(SmartDevice):
"""

LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
SET_LIGHT_METHOD = "transition_light_state"

def __init__(self, host: str) -> None:
super().__init__(host=host)
Expand Down Expand Up @@ -190,7 +192,7 @@ async def set_light_state(self, state: Dict, *, transition: int = None) -> Dict:
state["ignore_default"] = 1

light_state = await self._query_helper(
self.LIGHT_SERVICE, "transition_light_state", state
self.LIGHT_SERVICE, self.SET_LIGHT_METHOD, state
)
return light_state

Expand Down
6 changes: 6 additions & 0 deletions kasa/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class DeviceType(Enum):
Bulb = 2
Strip = 3
Dimmer = 4
LightStrip = 5
Unknown = -1


Expand Down Expand Up @@ -702,6 +703,11 @@ def is_bulb(self) -> bool:
"""Return True if the device is a bulb."""
return self._device_type == DeviceType.Bulb

@property
def is_light_strip(self) -> bool:
"""Return True if the device is a led strip."""
return self._device_type == DeviceType.LightStrip

@property
def is_plug(self) -> bool:
"""Return True if the device is a plug."""
Expand Down
75 changes: 75 additions & 0 deletions kasa/smartlightstrip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Module for light strips (KL430)."""
from typing import Any, Dict

from .smartbulb import SmartBulb
from .smartdevice import DeviceType, requires_update


class SmartLightStrip(SmartBulb):
"""Representation of a TP-Link Smart light strip.

Light strips work similarly to bulbs, but use a different service for controlling,
and expose some extra information (such as length and active effect).
This class extends :class:`SmartBulb` interface.

Examples:
>>> import asyncio
>>> strip = SmartLightStrip("127.0.0.1")
>>> asyncio.run(strip.update())
>>> print(strip.alias)
KL430 pantry lightstrip

Getting the length of the strip:

>>> strip.length
16

Currently active effect:

>>> strip.effect
{'brightness': 50, 'custom': 0, 'enable': 0, 'id': '', 'name': ''}

.. note::
The device supports some features that are not currently implemented,
feel free to find out how to control them and create a PR!


See :class:`SmartBulb` for more examples.
"""

LIGHT_SERVICE = "smartlife.iot.lightStrip"
SET_LIGHT_METHOD = "set_light_state"

def __init__(self, host: str) -> None:
super().__init__(host)
self._device_type = DeviceType.LightStrip

@property # type: ignore
@requires_update
def length(self) -> int:
"""Return length of the strip."""
return self.sys_info["length"]

@property # type: ignore
@requires_update
def effect(self) -> Dict:
"""Return effect state.

Example:
{'brightness': 50,
'custom': 0,
'enable': 0,
'id': '',
'name': ''}
"""
return self.sys_info["lighting_effect_state"]

@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return strip specific state information."""
info = super().state_information

info["Length"] = self.length

return info
30 changes: 25 additions & 5 deletions kasa/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/3342

from kasa import Discover, SmartBulb, SmartDimmer, SmartPlug, SmartStrip
from kasa import (
Discover,
SmartBulb,
SmartDimmer,
SmartLightStrip,
SmartPlug,
SmartStrip,
)

from .newfakes import FakeTransportProtocol

Expand All @@ -17,9 +24,11 @@
)


BULBS = {"KL60", "LB100", "LB120", "LB130", "KL120", "KL130"}
VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL130"}
COLOR_BULBS = {"LB130", "KL130"}
LIGHT_STRIPS = {"KL430"}
BULBS = {"KL60", "LB100", "LB120", "LB130", "KL120", "KL130", *LIGHT_STRIPS}
VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL130", "KL430", *LIGHT_STRIPS}
COLOR_BULBS = {"LB130", "KL130", *LIGHT_STRIPS}


PLUGS = {"HS100", "HS103", "HS105", "HS110", "HS200", "HS210"}
STRIPS = {"HS107", "HS300", "KP303", "KP400"}
Expand Down Expand Up @@ -65,9 +74,12 @@ def name_for_filename(x):
plug = parametrize("plugs", PLUGS, ids=name_for_filename)
strip = parametrize("strips", STRIPS, ids=name_for_filename)
dimmer = parametrize("dimmers", DIMMERS, ids=name_for_filename)
lightstrip = parametrize("lightstrips", LIGHT_STRIPS, ids=name_for_filename)

# This ensures that every single file inside fixtures/ is being placed in some category
categorized_fixtures = set(dimmer.args[1] + strip.args[1] + plug.args[1] + bulb.args[1])
categorized_fixtures = set(
dimmer.args[1] + strip.args[1] + plug.args[1] + bulb.args[1] + lightstrip.args[1]
)
diff = set(SUPPORTED_DEVICES) - set(categorized_fixtures)
if diff:
for file in diff:
Expand Down Expand Up @@ -105,12 +117,20 @@ def device_for_file(model):
for d in STRIPS:
if d in model:
return SmartStrip

for d in PLUGS:
if d in model:
return SmartPlug

# Light strips are recognized also as bulbs, so this has to go first
for d in LIGHT_STRIPS:
if d in model:
return SmartLightStrip

for d in BULBS:
if d in model:
return SmartBulb

for d in DIMMERS:
if d in model:
return SmartDimmer
Expand Down
70 changes: 70 additions & 0 deletions kasa/tests/fixtures/KL430(US)_1.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"emeter": {
"err_code": -1,
"err_msg": "module not support"
},
"smartlife.iot.common.emeter": {
"get_realtime": {
"current_ma": 0,
"err_code": 0,
"power_mw": 8729,
"total_wh": 21,
"voltage_mv": 0
}
},
"smartlife.iot.dimmer": {
"err_code": -1,
"err_msg": "module not support"
},
"smartlife.iot.smartbulb.lightingservice": {
"err_code": -1,
"err_msg": "module not support"
},
"system": {
"get_sysinfo": {
"active_mode": "none",
"alias": "KL430 pantry lightstrip",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
},
"description": "Kasa Smart Light Strip, Multicolor",
"dev_state": "normal",
"deviceId": "0000000000000000000000000000000000000000",
"disco_ver": "1.0",
"err_code": 0,
"hwId": "00000000000000000000000000000000",
"hw_ver": "1.0",
"is_color": 1,
"is_dimmable": 1,
"is_factory": false,
"is_variable_color_temp": 1,
"latitude_i": 0,
"length": 16,
"light_state": {
"brightness": 50,
"color_temp": 3630,
"hue": 0,
"mode": "normal",
"on_off": 1,
"saturation": 0
},
"lighting_effect_state": {
"brightness": 50,
"custom": 0,
"enable": 0,
"id": "",
"name": ""
},
"longitude_i": 0,
"mic_mac": "CC32E5230F55",
"mic_type": "IOT.SMARTBULB",
"model": "KL430(US)",
"oemId": "00000000000000000000000000000000",
"preferred_state": [],
"rssi": -56,
"status": "new",
"sw_ver": "1.0.10 Build 200522 Rel.104340"
}
}
}
Loading