Skip to content

Commit d30d00a

Browse files
Add support for lightstrips (KL430) (#74)
* Preliminary support for light strips * Add color temperature range and cleanup, thanks to @darkoppressor * Use lightstrip instead of {led,light}strip consistently everywhere * The cli flag is now --lightstrip * add apidocs * Add fixture file for KL430 Signed-off-by: Kevin Wells <darkoppressor@gmail.com> * Add discovery support, expose effect and length of the strip * use set_light_state instead of transition_light_state * Add tests for lightstrip * add doctests * Add KL430 to supported devices in README Co-authored-by: Kevin Wells <darkoppressor@gmail.com>
1 parent 0edbb43 commit d30d00a

File tree

15 files changed

+249
-15
lines changed

15 files changed

+249
-15
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,18 @@ or the `parse_pcap.py` script contained inside the `devtools` directory.
112112
* HS110
113113

114114
### Power Strips
115+
115116
* HS300
116117
* KP303
117118

118119
### Wall switches
120+
119121
* HS200
120122
* HS210
121123
* HS220
122124

123125
### Bulbs
126+
124127
* LB100
125128
* LB110
126129
* LB120
@@ -131,6 +134,10 @@ or the `parse_pcap.py` script contained inside the `devtools` directory.
131134
* KL120
132135
* KL130
133136

137+
### Light strips
138+
139+
* KL430
140+
134141
**Contributions (be it adding missing features, fixing bugs or improving documentation) are more than welcome, feel free to submit pull requests!**
135142

136143
### Resources

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ python-kasa documentation
1515
smartplug
1616
smartdimmer
1717
smartstrip
18+
smartlightstrip

docs/source/smartlightstrip.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Light strips
2+
============
3+
4+
.. autoclass:: kasa.SmartLightStrip
5+
:members:
6+
:undoc-members:

kasa/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from kasa.smartbulb import SmartBulb
1919
from kasa.smartdevice import DeviceType, EmeterStatus, SmartDevice
2020
from kasa.smartdimmer import SmartDimmer
21+
from kasa.smartlightstrip import SmartLightStrip
2122
from kasa.smartplug import SmartPlug
2223
from kasa.smartstrip import SmartStrip
2324

@@ -35,4 +36,5 @@
3536
"SmartPlug",
3637
"SmartStrip",
3738
"SmartDimmer",
39+
"SmartLightStrip",
3840
]

kasa/cli.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77

88
import asyncclick as click
99

10-
from kasa import Discover, SmartBulb, SmartDevice, SmartPlug, SmartStrip
10+
from kasa import (
11+
Discover,
12+
SmartBulb,
13+
SmartDevice,
14+
SmartLightStrip,
15+
SmartPlug,
16+
SmartStrip,
17+
)
1118

1219
click.anyio_backend = "asyncio"
1320

@@ -37,10 +44,11 @@
3744
@click.option("-d", "--debug", default=False, is_flag=True)
3845
@click.option("--bulb", default=False, is_flag=True)
3946
@click.option("--plug", default=False, is_flag=True)
47+
@click.option("--lightstrip", default=False, is_flag=True)
4048
@click.option("--strip", default=False, is_flag=True)
4149
@click.version_option()
4250
@click.pass_context
43-
async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
51+
async def cli(ctx, host, alias, target, debug, bulb, plug, lightstrip, strip):
4452
"""A tool for controlling TP-Link smart home devices.""" # noqa
4553
if debug:
4654
logging.basicConfig(level=logging.DEBUG)
@@ -64,7 +72,7 @@ async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
6472
await ctx.invoke(discover)
6573
return
6674
else:
67-
if not bulb and not plug and not strip:
75+
if not bulb and not plug and not strip and not lightstrip:
6876
click.echo("No --strip nor --bulb nor --plug given, discovering..")
6977
dev = await Discover.discover_single(host)
7078
elif bulb:
@@ -73,6 +81,8 @@ async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
7381
dev = SmartPlug(host)
7482
elif strip:
7583
dev = SmartStrip(host)
84+
elif lightstrip:
85+
dev = SmartLightStrip(host)
7686
else:
7787
click.echo("Unable to detect type, use --strip or --bulb or --plug!")
7888
return

kasa/discover.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from kasa.smartbulb import SmartBulb
1010
from kasa.smartdevice import SmartDevice, SmartDeviceException
1111
from kasa.smartdimmer import SmartDimmer
12+
from kasa.smartlightstrip import SmartLightStrip
1213
from kasa.smartplug import SmartPlug
1314
from kasa.smartstrip import SmartStrip
1415

@@ -227,11 +228,19 @@ def _get_device_class(info: dict) -> Type[SmartDevice]:
227228
and "get_dimmer_parameters" in info["smartlife.iot.dimmer"]
228229
):
229230
return SmartDimmer
231+
230232
elif "smartplug" in type_.lower() and "children" in sysinfo:
231233
return SmartStrip
234+
232235
elif "smartplug" in type_.lower():
236+
if "children" in sysinfo:
237+
return SmartStrip
238+
233239
return SmartPlug
234240
elif "smartbulb" in type_.lower():
241+
if "length" in sysinfo: # strips have length
242+
return SmartLightStrip
243+
235244
return SmartBulb
236245

237246
raise SmartDeviceException("Unknown device type: %s", type_)

kasa/smartbulb.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"KL130": (2500, 9000),
1818
r"KL120\(EU\)": (2700, 6500),
1919
r"KL120\(US\)": (2700, 5000),
20+
r"KL430\(US\)": (2500, 9000),
2021
}
2122

2223

@@ -89,6 +90,7 @@ class SmartBulb(SmartDevice):
8990
"""
9091

9192
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
93+
SET_LIGHT_METHOD = "transition_light_state"
9294

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

192194
light_state = await self._query_helper(
193-
self.LIGHT_SERVICE, "transition_light_state", state
195+
self.LIGHT_SERVICE, self.SET_LIGHT_METHOD, state
194196
)
195197
return light_state
196198

kasa/smartdevice.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class DeviceType(Enum):
3232
Bulb = 2
3333
Strip = 3
3434
Dimmer = 4
35+
LightStrip = 5
3536
Unknown = -1
3637

3738

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

706+
@property
707+
def is_light_strip(self) -> bool:
708+
"""Return True if the device is a led strip."""
709+
return self._device_type == DeviceType.LightStrip
710+
705711
@property
706712
def is_plug(self) -> bool:
707713
"""Return True if the device is a plug."""

kasa/smartlightstrip.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Module for light strips (KL430)."""
2+
from typing import Any, Dict
3+
4+
from .smartbulb import SmartBulb
5+
from .smartdevice import DeviceType, requires_update
6+
7+
8+
class SmartLightStrip(SmartBulb):
9+
"""Representation of a TP-Link Smart light strip.
10+
11+
Light strips work similarly to bulbs, but use a different service for controlling,
12+
and expose some extra information (such as length and active effect).
13+
This class extends :class:`SmartBulb` interface.
14+
15+
Examples:
16+
>>> import asyncio
17+
>>> strip = SmartLightStrip("127.0.0.1")
18+
>>> asyncio.run(strip.update())
19+
>>> print(strip.alias)
20+
KL430 pantry lightstrip
21+
22+
Getting the length of the strip:
23+
24+
>>> strip.length
25+
16
26+
27+
Currently active effect:
28+
29+
>>> strip.effect
30+
{'brightness': 50, 'custom': 0, 'enable': 0, 'id': '', 'name': ''}
31+
32+
.. note::
33+
The device supports some features that are not currently implemented,
34+
feel free to find out how to control them and create a PR!
35+
36+
37+
See :class:`SmartBulb` for more examples.
38+
"""
39+
40+
LIGHT_SERVICE = "smartlife.iot.lightStrip"
41+
SET_LIGHT_METHOD = "set_light_state"
42+
43+
def __init__(self, host: str) -> None:
44+
super().__init__(host)
45+
self._device_type = DeviceType.LightStrip
46+
47+
@property # type: ignore
48+
@requires_update
49+
def length(self) -> int:
50+
"""Return length of the strip."""
51+
return self.sys_info["length"]
52+
53+
@property # type: ignore
54+
@requires_update
55+
def effect(self) -> Dict:
56+
"""Return effect state.
57+
58+
Example:
59+
{'brightness': 50,
60+
'custom': 0,
61+
'enable': 0,
62+
'id': '',
63+
'name': ''}
64+
"""
65+
return self.sys_info["lighting_effect_state"]
66+
67+
@property # type: ignore
68+
@requires_update
69+
def state_information(self) -> Dict[str, Any]:
70+
"""Return strip specific state information."""
71+
info = super().state_information
72+
73+
info["Length"] = self.length
74+
75+
return info

kasa/tests/conftest.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88

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

11-
from kasa import Discover, SmartBulb, SmartDimmer, SmartPlug, SmartStrip
11+
from kasa import (
12+
Discover,
13+
SmartBulb,
14+
SmartDimmer,
15+
SmartLightStrip,
16+
SmartPlug,
17+
SmartStrip,
18+
)
1219

1320
from .newfakes import FakeTransportProtocol
1421

@@ -17,9 +24,11 @@
1724
)
1825

1926

20-
BULBS = {"KL60", "LB100", "LB120", "LB130", "KL120", "KL130"}
21-
VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL130"}
22-
COLOR_BULBS = {"LB130", "KL130"}
27+
LIGHT_STRIPS = {"KL430"}
28+
BULBS = {"KL60", "LB100", "LB120", "LB130", "KL120", "KL130", *LIGHT_STRIPS}
29+
VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL130", "KL430", *LIGHT_STRIPS}
30+
COLOR_BULBS = {"LB130", "KL130", *LIGHT_STRIPS}
31+
2332

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

6979
# This ensures that every single file inside fixtures/ is being placed in some category
70-
categorized_fixtures = set(dimmer.args[1] + strip.args[1] + plug.args[1] + bulb.args[1])
80+
categorized_fixtures = set(
81+
dimmer.args[1] + strip.args[1] + plug.args[1] + bulb.args[1] + lightstrip.args[1]
82+
)
7183
diff = set(SUPPORTED_DEVICES) - set(categorized_fixtures)
7284
if diff:
7385
for file in diff:
@@ -105,12 +117,20 @@ def device_for_file(model):
105117
for d in STRIPS:
106118
if d in model:
107119
return SmartStrip
120+
108121
for d in PLUGS:
109122
if d in model:
110123
return SmartPlug
124+
125+
# Light strips are recognized also as bulbs, so this has to go first
126+
for d in LIGHT_STRIPS:
127+
if d in model:
128+
return SmartLightStrip
129+
111130
for d in BULBS:
112131
if d in model:
113132
return SmartBulb
133+
114134
for d in DIMMERS:
115135
if d in model:
116136
return SmartDimmer

0 commit comments

Comments
 (0)