├── .gitignore ├── example_off.py ├── example_on.py ├── kelvin.png ├── example_simulator.py ├── lifx ├── __init__.py ├── enums.py ├── simulator.py ├── lifx.py ├── network.py └── packet.py ├── example_candle.py ├── example_image.py ├── example_intro.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /example_off.py: -------------------------------------------------------------------------------- 1 | import lifx 2 | 3 | lifx.Lifx().off() 4 | -------------------------------------------------------------------------------- /example_on.py: -------------------------------------------------------------------------------- 1 | import lifx 2 | 3 | lifx.Lifx().on() 4 | -------------------------------------------------------------------------------- /kelvin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arrian/lifx-python/HEAD/kelvin.png -------------------------------------------------------------------------------- /example_simulator.py: -------------------------------------------------------------------------------- 1 | from lifx.simulator import Simulator 2 | 3 | # This simulates the behaviour of a LIFX lightbulb locally. 4 | 5 | simulator = Simulator(bulb_label = b'Example Bulb') 6 | simulator.start() -------------------------------------------------------------------------------- /lifx/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['packet', 'network', 'simulator', 'enums', 'lifx'] 2 | 3 | from .enums import * 4 | from .packet import * 5 | from .network import Network 6 | from .simulator import Simulator 7 | from .lifx import Lifx 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example_candle.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import random 3 | import lifx 4 | 5 | lights = lifx.Lifx() 6 | lights.on() 7 | 8 | while True: 9 | lights.set_colour(random.randint(7000, 7500), 40000, random.randint(2200, 3000), 1000, random.randint(80, 180)) 10 | sleep(random.random() / 2) 11 | 12 | -------------------------------------------------------------------------------- /example_image.py: -------------------------------------------------------------------------------- 1 | # This example demonstrates setting the bulb colour to sampled pixels from an image. 2 | # Pillow (https://github.com/python-pillow/Pillow) or PIL is required to load images. 3 | from PIL import Image 4 | 5 | import lifx 6 | import colorsys 7 | from time import sleep 8 | 9 | # Connect to the bulb and ensure it is on 10 | lights = lifx.Lifx() 11 | lights.on() 12 | 13 | # Load the image 14 | im = Image.open('kelvin.png') 15 | pix = im.load() 16 | 17 | # Loop through the pixels 18 | for i in range(0,im.size[1]): 19 | for j in range(0,im.size[0]): 20 | rgb = pix[j,i] 21 | hsv = colorsys.rgb_to_hsv(rgb[0] / 256.0, rgb[1] / 256.0, rgb[2] / 256.0) 22 | 23 | lights.set_colour(hsv[0] * 60000, hsv[1] * 60000, hsv[2] * 60000, 1000, 200) 24 | 25 | print('rgb' + str(rgb)) 26 | 27 | sleep(0.02) 28 | 29 | -------------------------------------------------------------------------------- /lifx/enums.py: -------------------------------------------------------------------------------- 1 | 2 | class Service: 3 | UDP = 1 4 | TCP = 2 5 | 6 | class ResetSwitchPosition: 7 | UP = 0 8 | DOWN = 1 9 | 10 | class Interface: 11 | SOFT_AP = 1 12 | STATION = 2 13 | 14 | class WifiStatus: 15 | CONNECTING = 0 16 | CONNECTED = 1 17 | FAILED = 2 18 | OFF = 3 19 | 20 | class SecurityProtocol: 21 | OPEN = 1 22 | WEP_PSK = 2 23 | WPA_TKIP_PSK = 3 24 | WPA_AES_PSK = 4 25 | WPA2_AES_PSK = 5 26 | WPA2_TKIP_PSK = 6 27 | WPA2_MIXED_PSK = 7 28 | 29 | class Power: 30 | OFF = 0 31 | ON = 0xffff 32 | 33 | class Waveform: 34 | SAW = 0 35 | SINE = 1 36 | HALF_SINE = 2 37 | TRIANGLE = 3 38 | PULSE = 4 39 | 40 | class Colour: 41 | RED = 0 42 | ORANGE = 5000 43 | YELLOW = 8000 44 | GREEN = 18000 45 | BLUE = 42720 46 | 47 | class Brightness: 48 | OFF = 0 49 | DIM = 1000 50 | MOOD = 2500 51 | NORMAL = 6000 52 | FULL = 65000 53 | 54 | 55 | -------------------------------------------------------------------------------- /example_intro.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import lifx 3 | 4 | # Create the LIFX connection 5 | lights = lifx.Lifx() 6 | 7 | # Turn on the lights 8 | lights.on() 9 | 10 | # Set all the lights to green 11 | lights.set_colour(lifx.Colour.GREEN) 12 | 13 | # You can also be more specific with your colour settings: hue, saturation, brightness, kelvin, transition_duration 14 | lights.set_colour(0, 10000, 10000, 1000, 5000) 15 | 16 | # Get the lights' colours 17 | print(lights.get_colours()) 18 | 19 | # Get the lights' labels 20 | print(lights.get_labels()) 21 | 22 | # Get the lights' internal time 23 | print(lights.get_time()) 24 | 25 | # Get the wifi information 26 | print(lights.get_wifi_info()) 27 | 28 | # Get the available access points 29 | print(lights.get_access_points()) 30 | 31 | # Asynchronous print all LIFX network packets and continue. 32 | # Pass any function here that takes a one packet type argument. 33 | lights.monitor(lights.print_packet) 34 | 35 | while True: 36 | sleep(1) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LIFX Python Library 2 | 3 | This project aims to be a simple to use comprehensive python library for interaction with LIFX lightbulbs. Currently both Python 2 and Python 3 are supported. 4 | 5 | ## Usage 6 | 7 | ```python 8 | 9 | from time import sleep 10 | import lifx 11 | 12 | # Create the LIFX connection 13 | lights = lifx.Lifx() 14 | 15 | # Turn on the lights 16 | lights.on() 17 | 18 | # Set all the lights to green 19 | lights.set_colour(lifx.Colour.GREEN) 20 | 21 | # You can also be more specific with your colour settings: hue, saturation, brightness, kelvin, transition_duration 22 | lights.set_colour(0, 10000, 10000, 1000, 5000) 23 | 24 | # Get the lights' colours 25 | print(lights.get_colours()) 26 | 27 | # Get the lights' labels 28 | print(lights.get_labels()) 29 | 30 | # Get the lights' internal time 31 | print(lights.get_time()) 32 | 33 | # Get the wifi information 34 | print(lights.get_wifi_info()) 35 | 36 | # Get the available access points 37 | print(lights.get_access_points()) 38 | 39 | # Asynchronous print all LIFX network packets and continue. 40 | # Pass any function here that takes one argument. 41 | lights.monitor(lights.print_packet) 42 | 43 | while True: 44 | sleep(1) 45 | 46 | ``` -------------------------------------------------------------------------------- /lifx/simulator.py: -------------------------------------------------------------------------------- 1 | from .packet import * 2 | from .network import Network 3 | import string 4 | import random 5 | import threading 6 | import os 7 | 8 | try: 9 | import queue 10 | except ImportError: 11 | import Queue as queue # backwards compatibility for Python 2 12 | 13 | class Simulator: 14 | """ 15 | Simulates a LIFX bulb locally. 16 | """ 17 | network = None 18 | broadcast_queue = queue.Queue() 19 | 20 | def __init__(self, network = None, listen_port = 56700, mac_address = b'\xd0s\xd5\x00\xd4\xc3', hue = 0, saturation = 10000, brightness = 10000, kelvin = 1000, dim = 0, power = 1, wifi_info = (1.0, 0, 0, 10), wifi_firmware_state = (0, 0, 0), access_point = (0, b'Dummy Network', 0, 10, 10), bulb_label = b'LIFX Bulb Emulator #' + os.urandom(4), bulb_tags = [b'dummy1', b'dummy2']): 21 | self.network = network 22 | self.listen_port = listen_port 23 | self.mac_address = mac_address 24 | self.hue = hue 25 | self.saturation = saturation 26 | self.brightness = brightness 27 | self.kelvin = kelvin 28 | self.dim = dim 29 | self.power = power 30 | self.wifi_info = wifi_info 31 | self.wifi_firmware_state = wifi_firmware_state 32 | self.access_point = access_point 33 | self.bulb_label = bulb_label 34 | self.bulb_tags = bulb_tags 35 | 36 | if self.network is None: 37 | self.network = Network(False) 38 | 39 | def start(self): 40 | self.broadcast_pan() 41 | self.broadcast_status() 42 | self.broadcast_firmware_state() 43 | self.listen() 44 | 45 | def broadcast_pan(self): 46 | print('Broadcasting PAN update.') 47 | self.broadcast_queue.put(Packet.AsBulb(PacketType.PAN_GATEWAY, self.mac_address, self.mac_address, 1, self.listen_port)) 48 | threading.Timer(10, self.broadcast_pan).start() 49 | 50 | def broadcast_status(self): 51 | print('Broadcasting light status update.') 52 | self.broadcast_queue.put(Packet.AsBulb(PacketType.LIGHT_STATUS, self.mac_address, self.mac_address, self.hue, self.saturation, self.brightness, self.kelvin, self.dim, self.power, self.bulb_label, len(self.bulb_tags))) 53 | threading.Timer(20, self.broadcast_status).start() 54 | 55 | def broadcast_firmware_state(self): 56 | print('Broadcasting mesh firmware state update.') 57 | 58 | threading.Timer(20, self.broadcast_firmware_state).start() 59 | 60 | #listens and responds to requests 61 | def listen(self): 62 | while True: 63 | while not self.broadcast_queue.empty(): 64 | packet = self.broadcast_queue.get() 65 | print(str(packet)) 66 | self.network.broadcast(packet) 67 | self.network.listen_sync(PacketType.GET_PAN_GATEWAY, 1, 1) 68 | 69 | def get_time(self): 70 | int(round(time.time())) 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /lifx/lifx.py: -------------------------------------------------------------------------------- 1 | from .enums import * 2 | from .packet import * 3 | from .network import Network 4 | 5 | # Wraps network providing a simpler API. 6 | class Lifx: 7 | 8 | network = None 9 | 10 | def __init__(self): 11 | self.network = Network() 12 | 13 | def on(self): 14 | self.set_on(True) 15 | 16 | def off(self): 17 | self.set_on(False) 18 | 19 | def set_on(self, on): 20 | self.network.send(PacketType.SET_POWER_STATE, Power.ON if on else Power.OFF) 21 | 22 | def is_on(self): 23 | self.network.send(PacketType.GET_POWER_STATE) 24 | header, payload = self.network.listen_sync(PacketType.POWER_STATE, 1).get_data() 25 | 26 | if payload is not None: 27 | return payload.onoff 28 | return None 29 | 30 | def set_colour(self, hue, saturation = 30000, brightness = 30000, kelvin = 1000, duration = 500): 31 | self.network.send(PacketType.SET_LIGHT_COLOUR, 0, hue, saturation, brightness, kelvin, duration) 32 | 33 | def get_colours(self): 34 | self.network.send(PacketType.GET_LIGHT_STATE) 35 | header, payload = self.network.listen_sync(PacketType.LIGHT_STATUS, 1).get_data() 36 | 37 | if payload is not None: 38 | return payload.hue, payload.saturation, payload.brightness, payload.kelvin, payload.dim, payload.power, payload.build_label, payload.tags 39 | return None, None, None, None, None, None, None, None 40 | 41 | def get_wifi_info(self): 42 | self.network.send(PacketType.GET_WIFI_INFO) 43 | header, payload = self.network.listen_sync(PacketType.WIFI_INFO, 1).get_data() 44 | 45 | if payload is not None: 46 | return payload.signal, payload.tx, payload.rx, payload.mcu_temperature 47 | return None, None, None, None 48 | 49 | def get_labels(self): 50 | self.network.send(PacketType.GET_BULB_LABEL) 51 | packets = map(Packet.get_data, self.network.listen_sync(PacketType.BULB_LABEL, 10)) 52 | 53 | print(" ".join(map(str,[p[1].label.decode("utf-8") for p in packets]))) 54 | #if payload is not None: 55 | # pass 56 | #return payload.label.decode("utf-8") 57 | return None 58 | 59 | def get_access_points(self): 60 | self.network.send(PacketType.GET_ACCESS_POINTS) 61 | packets = map(Packet.get_data, self.network.listen_sync(PacketType.ACCESS_POINT, 10)) 62 | print(" ".join(map(str,[p[1].ssid.split(b'\0',1)[0].decode("utf-8") for p in packets]))) 63 | #if payload is not None: 64 | # pass 65 | #return payload.interface, payload.ssid.split(b'\0',1)[0].decode("utf-8"), payload.security_protocol, payload.strength, payload.channel 66 | return [(None, None, None, None, None)] 67 | 68 | def get_time(self): 69 | self.network.send(PacketType.GET_TIME) 70 | header, payload = self.network.listen_sync(PacketType.TIME_STATE, 1).get_data() 71 | 72 | if payload is not None: 73 | return payload.time / 1000000000 74 | return None 75 | 76 | def get_version(self): 77 | self.network.send(PacketType.GET_VERSION) 78 | header, payload = self.network.listen_sync(PacketType.VERSION_STATE, 1).get_data() 79 | 80 | if payload is not None: 81 | return payload.vendor, payload.product, payload.version 82 | return None, None, None 83 | 84 | def get_lights_by_tag(self, tag): 85 | pass 86 | 87 | def get_light_by_name(self, name): 88 | pass 89 | 90 | def reboot(self): 91 | self.network.send(PacketType.REBOOT) 92 | 93 | # Get the wrapped network object to perform lower level calls 94 | def get_network(self): 95 | return self.network 96 | 97 | # Monitors packets calling func when packet received 98 | def monitor(self, func): 99 | self.network.listen_async(func) 100 | 101 | def print_packet(self, packet): 102 | if packet is None: 103 | print('Packet could not be parsed.\n') 104 | else: 105 | print(str(packet) + '\n') 106 | 107 | 108 | -------------------------------------------------------------------------------- /lifx/network.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | from time import time, sleep 4 | import select 5 | from threading import Thread 6 | 7 | from .packet import * 8 | 9 | # handle tcp not supported 10 | import errno 11 | from socket import error as socket_error 12 | 13 | 14 | class Network: 15 | connection = None 16 | port = 56700 17 | ip = '0.0.0.0' 18 | broadcast_ip = '255.255.255.255' 19 | target = bytearray(6) 20 | site = bytearray(6) 21 | receive_size = 2048 22 | timeout = 2 23 | address = None 24 | 25 | def __init__(self, connect = True): 26 | if connect: 27 | self.connect() 28 | 29 | def to_mac(self, addr): 30 | return ':'.join('%02x' % b for b in addr) 31 | 32 | def check_connection(self): 33 | if self.connection is None: 34 | self.connect() 35 | if self.connection is None: 36 | raise Exception('Failed to connect.') 37 | 38 | def connect(self): 39 | udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 40 | udp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 41 | udp.bind((self.ip, self.port)) 42 | print(udp.getsockname()) 43 | 44 | p = Packet.ToBulb(PacketType.GET_PAN_GATEWAY, self.target, self.site, None) 45 | udp.sendto(bytes(p.get_bytes()), (self.broadcast_ip, self.port)) 46 | 47 | for x in range(10): 48 | try: 49 | udp.settimeout(self.timeout) 50 | data, self.address = udp.recvfrom(self.receive_size) 51 | packet = Packet.FromBulb(data) 52 | if packet is not None: 53 | header,payload = packet.get_data() 54 | if header.code is PacketType.PAN_GATEWAY.code: 55 | break 56 | except socket.timeout: 57 | raise Exception('Handshake timed out.') 58 | 59 | if header.code is not PacketType.PAN_GATEWAY.code: 60 | connection = None 61 | raise Exception('Connection failed. Could not determine the gateway.') 62 | 63 | print('Found light at %s:%s' % (self.address[0],self.address[1])) 64 | 65 | self.site = header.site 66 | try: 67 | tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 68 | tcp.settimeout(self.timeout) 69 | tcp.connect(self.address) 70 | tcp.setblocking(0) 71 | self.connection = tcp 72 | print('Using tcp') 73 | except socket_error as serr: 74 | if serr.errno != errno.ECONNREFUSED: 75 | raise serr 76 | self.connection = udp # latest versions of firmware >= 1.2 do not support tcp? fall back to udp 77 | print('Using udp') 78 | 79 | def disconnect(self): 80 | if self.connection is not None: 81 | self.connection.close() 82 | self.connection = None 83 | 84 | def send(self, packet_type, *data): 85 | self.check_connection() 86 | packet = Packet.ToBulb(packet_type, self.target, self.site, *data).get_bytes() 87 | 88 | if self.connection.type is socket.SOCK_DGRAM: 89 | self.connection.sendto(packet, self.address) 90 | else: 91 | self.connection.sendall(packet) 92 | 93 | def receive(self, timeout = 1): 94 | self.check_connection() 95 | ready = select.select([self.connection], [], [], timeout) 96 | if ready[0]: 97 | data = self.connection.recv(self.receive_size) 98 | return Packet.FromBulb(data) 99 | return None 100 | 101 | def broadcast(self, packet): 102 | broadcast_connection = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 103 | broadcast_connection.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 104 | broadcast_connection.bind((self.ip, 0)) 105 | broadcast_connection.sendto(bytes(packet.get_bytes()), (self.broadcast_ip, self.port)) 106 | broadcast_connection.close() 107 | 108 | # asynchronous method to call recv_func whenever a packet is received. 109 | def listen_async(self, recv_func, packet_filter = None, max_packets = None): 110 | self.check_connection() 111 | 112 | def perform_recv(val): 113 | packet_count = 0 114 | while max_packets is None or packet_count < max_packets: 115 | packet = self.receive() 116 | if packet is not None and (packet_filter is None or packet_filter.code is packet.packet_type.code): 117 | recv_func(packet) 118 | packet_count += 1 119 | 120 | thread = Thread(target = perform_recv, args = (10, )) 121 | thread.start() 122 | 123 | # Synchronous method to get the first of the specified packets. 124 | # Packet filter is a single packet code or a list of packet codes. 125 | def listen_sync(self, packet_filter, num_packets = 1, timeout = 2): 126 | self.check_connection() 127 | start_time = time() 128 | 129 | packets = [] 130 | 131 | while True: 132 | packet = self.receive() 133 | if packet is not None and (packet.packet_type.code is packet_filter.code or (isinstance(packet_filter, list) and packet.packet_type.code in packet_filter)): 134 | if num_packets == 1: 135 | return packet 136 | else: 137 | packets.append(packet) 138 | 139 | if timeout is not None and time() > start_time + timeout: 140 | break 141 | if num_packets == 1: 142 | return None 143 | return packets 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /lifx/packet.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections import namedtuple 3 | from struct import pack, unpack, calcsize 4 | 5 | class PacketDirection: 6 | FROM_BULB, TO_BULB = range(2) 7 | 8 | class PacketTypeDef(object): 9 | 10 | def __init__(self, text, code, payload, fmt, direction): 11 | self.text = text 12 | self.code = code 13 | self.payload = payload 14 | self.fmt = fmt 15 | self.direction = direction 16 | 17 | def __str__(self): 18 | return("%s Packet - %d" % (self.text, self.code)) 19 | 20 | class PacketTypeDefFromBulb(PacketTypeDef): 21 | def __init__(self, text, code, payload, fmt): 22 | super(PacketTypeDefFromBulb, self).__init__(text, code, payload, fmt, PacketDirection.FROM_BULB) 23 | 24 | class PacketTypeDefToBulb(PacketTypeDef): 25 | def __init__(self, text, code, payload, fmt): 26 | super(PacketTypeDefToBulb, self).__init__(text, code, payload, fmt, PacketDirection.TO_BULB) 27 | 28 | class PacketType: 29 | 30 | # Network 31 | GET_PAN_GATEWAY = PacketTypeDefToBulb('Get PAN Gateway', 0x02, None, None) 32 | PAN_GATEWAY = PacketTypeDefFromBulb('PAN Gateway', 0x03, 'service port', '