Skip to content

Commit 6d4f1ef

Browse files
Merge pull request #9 from soldag/master
Support for bridge v6 and RGBWW bulbs
2 parents b21edf5 + 96648c8 commit 6d4f1ef

File tree

13 files changed

+1341
-215
lines changed

13 files changed

+1341
-215
lines changed

Diff for: .gitignore

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
env/
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
28+
# PyInstaller
29+
# Usually these files are written by a python script from a template
30+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
31+
*.manifest
32+
*.spec
33+
34+
# Installer logs
35+
pip-log.txt
36+
pip-delete-this-directory.txt
37+
38+
# Unit test / coverage reports
39+
htmlcov/
40+
.tox/
41+
.coverage
42+
.coverage.*
43+
.cache
44+
nosetests.xml
45+
coverage.xml
46+
*,cover
47+
.hypothesis/
48+
49+
# Translations
50+
*.mo
51+
*.pot
52+
53+
# Django stuff:
54+
*.log
55+
local_settings.py
56+
57+
# Flask stuff:
58+
instance/
59+
.webassets-cache
60+
61+
# Scrapy stuff:
62+
.scrapy
63+
64+
# Sphinx documentation
65+
docs/_build/
66+
67+
# PyBuilder
68+
target/
69+
70+
# Jupyter Notebook
71+
.ipynb_checkpoints
72+
73+
# pyenv
74+
.python-version
75+
76+
# celery beat schedule file
77+
celerybeat-schedule
78+
79+
# dotenv
80+
.env
81+
82+
# virtualenv
83+
.venv/
84+
venv/
85+
ENV/
86+
87+
# Spyder project settings
88+
.spyderproject
89+
90+
# Rope project settings
91+
.ropeproject
92+
93+
# IntelliJ project settings
94+
.idea

Diff for: README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# python-limitlessled
22

3-
`python-limitlessled` controls LimitlessLED bridges. It supports `white` and `rgbw` bulb groups.
3+
`python-limitlessled` controls LimitlessLED bridges. It supports `white`, `rgbw` and `rgbww` bulb groups as well as the `bridge-led` of newer wifi bridges.
44
## Install
55
`pip install limitlessled`
66

@@ -12,13 +12,15 @@ Group names can be any string, but must be unique amongst all bridges.
1212
```python
1313
from limitlessled.bridge import Bridge
1414
from limitlessled.group.rgbw import RGBW
15+
from limitlessled.group.rgbww import RGBWW
1516
from limitlessled.group.white import WHITE
1617

1718
bridge = Bridge('<your bridge ip address>')
1819
bridge.add_group(1, 'bedroom', RGBW)
1920
# A group number can support two groups as long as the types differ
2021
bridge.add_group(2, 'bathroom', WHITE)
2122
bridge.add_group(2, 'living_room', RGBW)
23+
bridge.add_group(2, 'kitchen', RGBWW)
2224
```
2325

2426
Get access to groups either via the return value of `add_group`, or with the `LimitlessLED` object.
@@ -29,6 +31,9 @@ bedroom = bridge.add_group(1, 'bedroom', RGBW)
2931
limitlessled = LimitlessLED()
3032
limitlessled.add_bridge(bridge)
3133
bedroom = limitlessled.group('bedroom')
34+
35+
# The bridge led can be controlled and acts as a RGBW group
36+
bridge_led = bridge.bridge_led
3237
```
3338

3439
### Control

Diff for: limitlessled/bridge.py

+149-27
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@
44
import socket
55
import time
66
import threading
7+
from datetime import datetime, timedelta
78

89
from limitlessled import MIN_WAIT, REPS
9-
from limitlessled.group.rgbw import RgbwGroup, RGBW
10+
from limitlessled.group.rgbw import RgbwGroup, RGBW, BRIDGE_LED
11+
from limitlessled.group.rgbww import RgbwwGroup, RGBWW
1012
from limitlessled.group.white import WhiteGroup, WHITE
1113

1214

13-
BRIDGE_PORT = 8899
14-
BRIDGE_VERSION = 5
15-
BRIDGE_SHORT_VERSION_MIN = 3
16-
BRIDGE_LONG_BYTE = 0x55
15+
BRIDGE_PORT = 5987
16+
BRIDGE_VERSION = 6
17+
BRIDGE_LED_GROUP = 1
18+
BRIDGE_LED_NAME = 'bridge'
1719
SELECT_WAIT = 0.025
20+
BRIDGE_INITIALIZATION_COMMAND = [0x20, 0x00, 0x00, 0x00, 0x16, 0x02, 0x62,
21+
0x3a, 0xd5, 0xed, 0xa3, 0x01, 0xae, 0x08,
22+
0x2d, 0x46, 0x61, 0x41, 0xa7, 0xf6, 0xdc,
23+
0xaf, 0xfe, 0xf7, 0x00, 0x00, 0x1e]
24+
KEEP_ALIVE_COMMAND_PREAMBLE = [0xD0, 0x00, 0x00, 0x00, 0x02]
25+
KEEP_ALIVE_RESPONSE_PREAMBLE = [0xd8, 0x0, 0x0, 0x0, 0x07]
26+
KEEP_ALIVE_TIME = 1
27+
RECONNECT_TIME = 5
28+
SOCKET_TIMEOUT = 5
29+
STARTING_SEQUENTIAL_BYTE = 0x02
1830

1931

2032
def group_factory(bridge, number, name, led_type):
@@ -23,11 +35,13 @@ def group_factory(bridge, number, name, led_type):
2335
:param bridge: Member of this bridge.
2436
:param number: Group number (1-4).
2537
:param name: Name of group.
26-
:param led_type: Either `RGBW` or `WHITE`.
38+
:param led_type: Either `RGBW`, `RGBWW`, `WHITE` or `BRIDGE_LED`.
2739
:returns: New group.
2840
"""
29-
if led_type == RGBW:
30-
return RgbwGroup(bridge, number, name)
41+
if led_type in [RGBW, BRIDGE_LED]:
42+
return RgbwGroup(bridge, number, name, led_type)
43+
elif led_type == RGBWW:
44+
return RgbwwGroup(bridge, number, name)
3145
elif led_type == WHITE:
3246
return WhiteGroup(bridge, number, name)
3347
else:
@@ -37,34 +51,78 @@ def group_factory(bridge, number, name, led_type):
3751
class Bridge(object):
3852
""" Represents a LimitlessLED bridge. """
3953

40-
def __init__(self, ip, port=BRIDGE_PORT, version=BRIDGE_VERSION):
54+
def __init__(self, ip, port=BRIDGE_PORT, version=BRIDGE_VERSION,
55+
bridge_led_name=BRIDGE_LED_NAME):
4156
""" Initialize bridge.
4257
43-
Bridge version 3 through 5 (latest as of this release)
58+
Bridge version 6 (latest as of this release)
4459
can use the default parameters. For lower versions,
45-
use port 50000. Lower versions also require sending a
46-
larger payload to the bridge (slower).
60+
use port 8899 (3 to 5) or 50000 (lower then 3).
61+
Lower versions also require sending a larger payload
62+
to the bridge (slower).
4763
4864
:param ip: IP address of bridge.
4965
:param port: Bridge port.
5066
:param version: Bridge version.
67+
:param bridge_led_name: Name of the bridge led group.
5168
"""
69+
self.is_closed = False
5270
self.wait = MIN_WAIT
5371
self.reps = REPS
5472
self.groups = []
5573
self.ip = ip
5674
self.version = version
75+
self._sn = STARTING_SEQUENTIAL_BYTE
5776
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
77+
self._socket.settimeout(SOCKET_TIMEOUT)
5878
self._socket.connect((ip, port))
5979
self._command_queue = queue.Queue()
6080
self._lock = threading.Lock()
6181
self.active = 0
6282
self._selected_number = None
83+
6384
# Start queue consumer thread.
6485
consumer = threading.Thread(target=self._consume)
6586
consumer.daemon = True
6687
consumer.start()
6788

89+
# Version specific stuff
90+
self._wb1 = None
91+
self._wb2 = None
92+
self._bridge_led = None
93+
if self.version >= 6:
94+
# Create bridge led group
95+
self._bridge_led = group_factory(self, BRIDGE_LED_GROUP,
96+
bridge_led_name, BRIDGE_LED)
97+
98+
# Initialize connection to retrieve bridge session ids (wb1, wb2)
99+
self._init_connection()
100+
101+
# Start keep alive thread.
102+
keep_alive_thread = threading.Thread(target=self._keep_alive)
103+
keep_alive_thread.daemon = True
104+
keep_alive_thread.start()
105+
106+
@property
107+
def sn(self):
108+
""" Gets the current sequential byte. """
109+
return self._sn
110+
111+
@property
112+
def wb1(self):
113+
""" Gets the bridge session id 1. """
114+
return self._wb1
115+
116+
@property
117+
def wb2(self):
118+
""" Gets the bridge session id 2. """
119+
return self._wb2
120+
121+
@property
122+
def bridge_led(self):
123+
""" Get the group to control the bridge led. """
124+
return self._bridge_led
125+
68126
def incr_active(self):
69127
""" Increment number of active groups. """
70128
with self._lock:
@@ -87,21 +145,19 @@ def add_group(self, number, name, led_type):
87145
self.groups.append(group)
88146
return group
89147

90-
def send(self, group, command, reps=REPS, wait=MIN_WAIT, select=False):
148+
def send(self, command, reps=REPS, wait=MIN_WAIT):
91149
""" Send a command to the physical bridge.
92150
93-
:param group: Run on this group.
94-
:param command: A bytearray.
151+
:param command: A Command instance.
95152
:param reps: Number of repetitions.
96153
:param wait: Wait time in seconds.
97-
:param select: Select group if necessary.
98154
"""
99155
# Enqueue the command.
100-
self._command_queue.put((group, command, reps, wait, select))
156+
self._command_queue.put((command, reps, wait))
101157
# Wait before accepting another command.
102-
# This keeps indvidual groups relatively synchronized.
158+
# This keeps individual groups relatively synchronized.
103159
sleep = reps * wait * self.active
104-
if select and self._selected_number != group.number:
160+
if command.select and self._selected_number != command.group_number:
105161
sleep += SELECT_WAIT
106162
time.sleep(sleep)
107163

@@ -118,17 +174,83 @@ def _consume(self):
118174
119175
TODO: Only wait when another command comes in.
120176
"""
121-
while True:
177+
while not self.is_closed:
122178
# Get command from queue.
123-
(group, command, reps, wait, select) = self._command_queue.get()
179+
(command, reps, wait) = self._command_queue.get()
124180
# Select group if a different group is currently selected.
125-
if select and self._selected_number != group.number:
126-
self._socket.send(bytearray(group.get_select_cmd()))
181+
if command.select and self._selected_number != command.group_number:
182+
self._send_raw(command.select_command.bytes)
127183
time.sleep(SELECT_WAIT)
128184
# Repeat command as necessary.
129185
for _ in range(reps):
130-
if self.version < BRIDGE_SHORT_VERSION_MIN:
131-
command.append(BRIDGE_LONG_BYTE)
132-
self._socket.send(bytearray(command))
186+
self._send_raw(command.bytes)
133187
time.sleep(wait)
134-
self._selected_number = group.number
188+
self._selected_number = command.group_number
189+
190+
def _send_raw(self, command):
191+
"""
192+
Sends an raw command directly to the physical bridge.
193+
:param command: A bytearray.
194+
"""
195+
self._socket.send(bytearray(command))
196+
self._sn = (self._sn + 1) % 256
197+
198+
def _init_connection(self):
199+
"""
200+
Requests the session ids of the bridge.
201+
:returns: True, if initialization was successful. False, otherwise.
202+
"""
203+
try:
204+
response = bytearray(22)
205+
self._send_raw(BRIDGE_INITIALIZATION_COMMAND)
206+
self._socket.recv_into(response)
207+
self._wb1 = response[19]
208+
self._wb2 = response[20]
209+
except socket.timeout:
210+
return False
211+
212+
return True
213+
214+
def _reconnect(self):
215+
"""
216+
Try continuously to reconnect to the bridge.
217+
"""
218+
while not self.is_closed:
219+
if self._init_connection():
220+
return
221+
222+
time.sleep(RECONNECT_TIME)
223+
224+
def _keep_alive(self):
225+
"""
226+
Send keep alive messages continuously to bridge.
227+
"""
228+
while not self.is_closed:
229+
command = KEEP_ALIVE_COMMAND_PREAMBLE + [self.wb1, self.wb2]
230+
self._send_raw(command)
231+
232+
start = datetime.now()
233+
connection_alive = False
234+
while datetime.now() - start < timedelta(seconds=SOCKET_TIMEOUT):
235+
response = bytearray(12)
236+
try:
237+
self._socket.recv_into(response)
238+
except socket.timeout:
239+
break
240+
241+
if response[:5] == bytearray(KEEP_ALIVE_RESPONSE_PREAMBLE):
242+
connection_alive = True
243+
break
244+
245+
if not connection_alive:
246+
self._reconnect()
247+
continue
248+
249+
time.sleep(KEEP_ALIVE_TIME)
250+
251+
def close(self):
252+
"""
253+
Closes the connection to the bridge.
254+
"""
255+
self.is_closed = True
256+

0 commit comments

Comments
 (0)