Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
50b5627
Add support for v6.
soldag Dec 22, 2016
6189135
Wrap select flag in Command class to set flag in command sets.
soldag Dec 23, 2016
7de66a9
Fixed bug in color conversion for v6.
soldag Dec 31, 2016
ff3e459
Corrected v6 color conversion.
soldag Dec 31, 2016
558d895
Switched commands for rgbw and rgbww.
soldag Dec 31, 2016
939a43f
Support for RGBWW saturation
ash-vd Dec 31, 2016
1a39ca2
Ignored .pyc-files
ash-vd Dec 31, 2016
7bb343b
Fixed setup.py.
soldag Jan 2, 2017
16fb1b0
Implemented keep alive messages.
soldag Jan 2, 2017
c2ab1ee
Merge remote-tracking branch 'upstream/master'
ash-vd Jan 2, 2017
0f3e659
Convert the saturation for RGBWW bulbs
ash-vd Jan 2, 2017
8b07e34
Reset the Sequence Number to 0 when it passed 255.
soldag Jan 3, 2017
423f9d4
Merge pull request #1 from ashwinvandijk/master
soldag Jan 3, 2017
6ee34de
Updated .gitignore.
soldag Jan 4, 2017
7f8dedd
Created separate RGBWW group.
soldag Jan 4, 2017
05217f2
Settings color for RGBWW bulbs changes hue and saturation.
soldag Jan 4, 2017
1b90840
Fixed color conversion for RGBWW bulbs.
soldag Jan 4, 2017
5342f9e
Minor fix.
soldag Jan 4, 2017
2aef1f1
Fixed setting wait and reps with rate decorator.
soldag Jan 4, 2017
11617d9
Adopt transition logic to latest changes.
soldag Jan 4, 2017
ec01edf
The temperature of RGBWW bulbs is now restored when switching into wh…
soldag Jan 4, 2017
9c6a7e5
Support for hue and saturation in pipelines.
soldag Jan 4, 2017
60c7acd
Implemented reconnection if connection to bridge is lost.
soldag Jan 9, 2017
cf9f7a0
Fixed rgbww transitions.
soldag Jan 15, 2017
9455a41
Update color property when setting hue or saturation manually.
soldag Jan 30, 2017
02250e7
Setting RGBW lamp to white, if saturation is 0.
soldag Jan 30, 2017
2062a07
Fix transition steps.
soldag Jan 30, 2017
fc081f9
Reconnect, if keep alive response is invalid.
soldag Jan 30, 2017
c6c7929
Fix receiving keep-alive responses. When sending a command, execution…
soldag Jan 31, 2017
ce4373d
Changed order of setting properties for transitions.
soldag Jan 31, 2017
ab8a00d
Fixed transition timing.
soldag Feb 8, 2017
91ff3b3
Updated docs.
soldag Feb 8, 2017
cda16c2
Removed duplicate lines.
soldag Feb 9, 2017
ecb0e57
Fixed white leds used with bridge v6.
soldag Feb 9, 2017
7931fca
Fixed setting hue in legacy command set.
soldag Feb 10, 2017
d26cc68
Removed blank line.
soldag Feb 10, 2017
e288b42
Removed unused import.
soldag Feb 10, 2017
9e0f653
Fixed selection of group when sending a command.
soldag Feb 10, 2017
96648c8
Fixed setting brightness and temperature of white bulbs.
soldag Feb 11, 2017
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
94 changes: 94 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# dotenv
.env

# virtualenv
.venv/
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

# IntelliJ project settings
.idea
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# python-limitlessled

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

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

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

Get access to groups either via the return value of `add_group`, or with the `LimitlessLED` object.
Expand All @@ -29,6 +31,9 @@ bedroom = bridge.add_group(1, 'bedroom', RGBW)
limitlessled = LimitlessLED()
limitlessled.add_bridge(bridge)
bedroom = limitlessled.group('bedroom')

# The bridge led can be controlled and acts as a RGBW group
bridge_led = bridge.bridge_led
```

### Control
Expand Down
176 changes: 149 additions & 27 deletions limitlessled/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@
import socket
import time
import threading
from datetime import datetime, timedelta

from limitlessled import MIN_WAIT, REPS
from limitlessled.group.rgbw import RgbwGroup, RGBW
from limitlessled.group.rgbw import RgbwGroup, RGBW, BRIDGE_LED
from limitlessled.group.rgbww import RgbwwGroup, RGBWW
from limitlessled.group.white import WhiteGroup, WHITE


BRIDGE_PORT = 8899
BRIDGE_VERSION = 5
BRIDGE_SHORT_VERSION_MIN = 3
BRIDGE_LONG_BYTE = 0x55
BRIDGE_PORT = 5987
BRIDGE_VERSION = 6
BRIDGE_LED_GROUP = 1
BRIDGE_LED_NAME = 'bridge'
SELECT_WAIT = 0.025
BRIDGE_INITIALIZATION_COMMAND = [0x20, 0x00, 0x00, 0x00, 0x16, 0x02, 0x62,
0x3a, 0xd5, 0xed, 0xa3, 0x01, 0xae, 0x08,
0x2d, 0x46, 0x61, 0x41, 0xa7, 0xf6, 0xdc,
0xaf, 0xfe, 0xf7, 0x00, 0x00, 0x1e]
KEEP_ALIVE_COMMAND_PREAMBLE = [0xD0, 0x00, 0x00, 0x00, 0x02]
KEEP_ALIVE_RESPONSE_PREAMBLE = [0xd8, 0x0, 0x0, 0x0, 0x07]
KEEP_ALIVE_TIME = 1
RECONNECT_TIME = 5
SOCKET_TIMEOUT = 5
STARTING_SEQUENTIAL_BYTE = 0x02


def group_factory(bridge, number, name, led_type):
Expand All @@ -23,11 +35,13 @@ def group_factory(bridge, number, name, led_type):
:param bridge: Member of this bridge.
:param number: Group number (1-4).
:param name: Name of group.
:param led_type: Either `RGBW` or `WHITE`.
:param led_type: Either `RGBW`, `RGBWW`, `WHITE` or `BRIDGE_LED`.
:returns: New group.
"""
if led_type == RGBW:
return RgbwGroup(bridge, number, name)
if led_type in [RGBW, BRIDGE_LED]:
return RgbwGroup(bridge, number, name, led_type)
elif led_type == RGBWW:
return RgbwwGroup(bridge, number, name)
elif led_type == WHITE:
return WhiteGroup(bridge, number, name)
else:
Expand All @@ -37,34 +51,78 @@ def group_factory(bridge, number, name, led_type):
class Bridge(object):
""" Represents a LimitlessLED bridge. """

def __init__(self, ip, port=BRIDGE_PORT, version=BRIDGE_VERSION):
def __init__(self, ip, port=BRIDGE_PORT, version=BRIDGE_VERSION,
bridge_led_name=BRIDGE_LED_NAME):
""" Initialize bridge.

Bridge version 3 through 5 (latest as of this release)
Bridge version 6 (latest as of this release)
can use the default parameters. For lower versions,
use port 50000. Lower versions also require sending a
larger payload to the bridge (slower).
use port 8899 (3 to 5) or 50000 (lower then 3).
Lower versions also require sending a larger payload
to the bridge (slower).

:param ip: IP address of bridge.
:param port: Bridge port.
:param version: Bridge version.
:param bridge_led_name: Name of the bridge led group.
"""
self.is_closed = False
self.wait = MIN_WAIT
self.reps = REPS
self.groups = []
self.ip = ip
self.version = version
self._sn = STARTING_SEQUENTIAL_BYTE
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._socket.settimeout(SOCKET_TIMEOUT)
self._socket.connect((ip, port))
self._command_queue = queue.Queue()
self._lock = threading.Lock()
self.active = 0
self._selected_number = None

# Start queue consumer thread.
consumer = threading.Thread(target=self._consume)
consumer.daemon = True
consumer.start()

# Version specific stuff
self._wb1 = None
self._wb2 = None
self._bridge_led = None
if self.version >= 6:
# Create bridge led group
self._bridge_led = group_factory(self, BRIDGE_LED_GROUP,
bridge_led_name, BRIDGE_LED)

# Initialize connection to retrieve bridge session ids (wb1, wb2)
self._init_connection()

# Start keep alive thread.
keep_alive_thread = threading.Thread(target=self._keep_alive)
keep_alive_thread.daemon = True
keep_alive_thread.start()

@property
def sn(self):
""" Gets the current sequential byte. """
return self._sn

@property
def wb1(self):
""" Gets the bridge session id 1. """
return self._wb1

@property
def wb2(self):
""" Gets the bridge session id 2. """
return self._wb2

@property
def bridge_led(self):
""" Get the group to control the bridge led. """
return self._bridge_led

def incr_active(self):
""" Increment number of active groups. """
with self._lock:
Expand All @@ -87,21 +145,19 @@ def add_group(self, number, name, led_type):
self.groups.append(group)
return group

def send(self, group, command, reps=REPS, wait=MIN_WAIT, select=False):
def send(self, command, reps=REPS, wait=MIN_WAIT):
""" Send a command to the physical bridge.

:param group: Run on this group.
:param command: A bytearray.
:param command: A Command instance.
:param reps: Number of repetitions.
:param wait: Wait time in seconds.
:param select: Select group if necessary.
"""
# Enqueue the command.
self._command_queue.put((group, command, reps, wait, select))
self._command_queue.put((command, reps, wait))
# Wait before accepting another command.
# This keeps indvidual groups relatively synchronized.
# This keeps individual groups relatively synchronized.
sleep = reps * wait * self.active
if select and self._selected_number != group.number:
if command.select and self._selected_number != command.group_number:
sleep += SELECT_WAIT
time.sleep(sleep)

Expand All @@ -118,17 +174,83 @@ def _consume(self):

TODO: Only wait when another command comes in.
"""
while True:
while not self.is_closed:
# Get command from queue.
(group, command, reps, wait, select) = self._command_queue.get()
(command, reps, wait) = self._command_queue.get()
# Select group if a different group is currently selected.
if select and self._selected_number != group.number:
self._socket.send(bytearray(group.get_select_cmd()))
if command.select and self._selected_number != command.group_number:
self._send_raw(command.select_command.bytes)
time.sleep(SELECT_WAIT)
# Repeat command as necessary.
for _ in range(reps):
if self.version < BRIDGE_SHORT_VERSION_MIN:
command.append(BRIDGE_LONG_BYTE)
self._socket.send(bytearray(command))
self._send_raw(command.bytes)
time.sleep(wait)
self._selected_number = group.number
self._selected_number = command.group_number

def _send_raw(self, command):
"""
Sends an raw command directly to the physical bridge.
:param command: A bytearray.
"""
self._socket.send(bytearray(command))
self._sn = (self._sn + 1) % 256

def _init_connection(self):
"""
Requests the session ids of the bridge.
:returns: True, if initialization was successful. False, otherwise.
"""
try:
response = bytearray(22)
self._send_raw(BRIDGE_INITIALIZATION_COMMAND)
self._socket.recv_into(response)
self._wb1 = response[19]
self._wb2 = response[20]
except socket.timeout:
return False

return True

def _reconnect(self):
"""
Try continuously to reconnect to the bridge.
"""
while not self.is_closed:
if self._init_connection():
return

time.sleep(RECONNECT_TIME)

def _keep_alive(self):
"""
Send keep alive messages continuously to bridge.
"""
while not self.is_closed:
command = KEEP_ALIVE_COMMAND_PREAMBLE + [self.wb1, self.wb2]
self._send_raw(command)

start = datetime.now()
connection_alive = False
while datetime.now() - start < timedelta(seconds=SOCKET_TIMEOUT):
response = bytearray(12)
try:
self._socket.recv_into(response)
except socket.timeout:
break

if response[:5] == bytearray(KEEP_ALIVE_RESPONSE_PREAMBLE):
connection_alive = True
break

if not connection_alive:
self._reconnect()
continue

time.sleep(KEEP_ALIVE_TIME)

def close(self):
"""
Closes the connection to the bridge.
"""
self.is_closed = True

Loading