Welcome to documentation for XLED - Smart LED Christmas lights!

Contents:

XLED - unofficial control of Twinkly - Smart Decoration LED lights

XLED is a python library and command line interface (CLI) to control Twinkly - Smart Decoration LED lights for Christmas.

Official materials says:

Twinkly is a LED light device that you can control via smartphone. It allows you to play with colouful and animated effects, or create new ones. Decoration lights, not suitable for household illumination.

Since its Kickstarter project in 2016 many products were introduced with varying properties and features. Most notably products released since September 2019 are identified as Generation II. Older products are since then referred as Generation I.

Library and CLI are free software available under MIT license.

Installation

Both library and CLI tool are supported on Linux, primarily Fedora.

  1. First make sure that you have pip installed. E.g. for Fedora:

    $ sudo dnf install python3-pip python3-wheel
    
  2. You might want to create and activate a virtual environment. E.g.:

    $ mkdir -p ~/.virtualenvs
    $ python3 -m venv ~/.virtualenvs/xled
    $ source ~/.virtualenvs/xled/bin/activate
    
  3. Install xled from PyPI:

    $ python3 -m pip install --upgrade xled
    

Usage

If you have installed the project into virtual environment, activate it first. E.g.

Use of the library:

>>> import xled
>>> discovered_device = xled.discover.discover()
>>> discovered_device.id
'Twinkly_33AAFF'
>>> control = xled.ControlInterface(discovered_device.ip_address, discovered_device.hw_address)
>>> control.set_mode('movie')
<ApplicationResponse [1000]>
>>> control.get_mode()['mode']
'movie'
>>> control.get_device_info()['number_of_led']
210

Documentation for the library can be found online.

Use of the CLI:

$ xled on
Looking for any device...
Working on device: Twinkly_33AAFF
Turned on.

For more commands and options see xled –help.

Why?

My first Twinkly was 105 LEDs starter light set. That was the latest available model in 2017: TW105S-EU. As of December 2017 there are only two ways to control lights: mobile app on Android or iOS or hardware button on the cord.

Android application didn’t work as advertised on my Xiaomi Redmi 3S phone. On first start it connected and disconnected in very fast pace (like every 1-2 seconds) to the hardware. I wasn’t able to control anything at all. Later I wanted to connect it to my local WiFi network. But popup dialog that shouldn’t have appear never did so.

Public API was promised around Christmas 2016 for next season. Later update from October 2016 it seems API won’t be available any time soon:

API for external control are on our dev check list, we definitely need some feedback from the community to understand which could be a proper core set to start with.

It turned out that application uses HTTP to control lights. I ended up with capturing network traffic and documented this private API. In the end I’m able to configure the device pretty easilly.

As of 2020 Twinkly devices can be controlled by Amazon Alexa and Google Assistant as well. Mobile application now requires an account to operate lights even locally. No sign of public API for local devices though. Therefore with my second device - Twinkly 210 RGB+W Wall I keep improving this library and CLI documentation to be able to operate my devices locally and not rely on availability of manufacturer’s servers.

Credits

This package was created with Cookiecutter and the audreyr/cookiecutter-pypackage project template.

Join the chat at https://gitter.im/xled-community/chat

Support

Supported Operating Systems

Generally both CLI and library is supported only on Linux. Most often code is tested supported Fedora and CI usually runs on Ubuntu.

It might work on other platforms where python runs but there is no guarantee. No special code for other platforms will be added.

Supported Python Versions

Both CLI and library are written in python.

Python 2

Project supports python 2.7. Python 2 will be supported as long as libraries project depends on are reasonably available and possible to support with python 3 as well.

Python 3

Following versions of python 3 are supported:

  • 3.7
  • 3.8
  • 3.9
  • 3.10
  • 3.11

Python 3 support will more or less follow python 3 lifecycle - if new version is released project should run on it as well. As python version goes end of life from upstream the support by the project will fade away as well. See Status of Python branches for upstream lifecycle.

Supported Devices

Generally device models that are listed in Hardware section of xled-docs are supported. Other models might work but there is no guarantee.

Command Line Interface

CLI Guide

Primary goal of command line interface is to provide high level way to query and control devices. It provides subset of features that devices are capable of. Tool doesn’t keep any configuration and before each operation it discovers available devices. By default first device that responds is being controlled. Alternatively specified device can be controlled.

Run xled –help to get available options.

Python API Documentation / Guide

xled package

Submodules

xled.auth module

xled.auth

Custom authentication handler and authenticated session to be used with requests.Session

class xled.auth.BaseUrlChallengeResponseAuthSession(hw_address=None, client=None, auto_refresh_token=True, **kwargs)[source]

Bases: requests_toolbelt.sessions.BaseUrlSession

Extension to requests_toolbelt.BaseUrlSession to provide authentication.

Any request used with this session gets authentication token added. Authentication token can be fetched even separately.

access_token

Current authentication token if exists. None if it wasn’t fetched yet.

add_authorization(headers)[source]

Returns headers with added authorization

Parameters:headers (dict) – user supplied request headers
Return type:dict
add_token(headers=None)[source]

Adds token header to dictionary with headers

Parameters:headers (dict) – Optional initial dictionary with headers.
Returns:Dict with added authentication header.
Return type:dict
Raises:TokenExpiredError – If token is expected to be expired.
authorized

Boolean that indicates whether this session has an ChallengeResponse token or not. If self.authorized is True, you can reasonably expect ChallengeResponse-protected requests to the resource to succeed. If self.authorized is False, you need the user to go through the ChallengeResponse authentication dance before ChallengeResponse-protected requests to the resource will succeed. :rtype: bool

challenge_url

Full URL of login endpoint

Returns:String with full url
Return type:str
fetch_token()[source]

Main authentication method that fetches new token

Returns:Token as string.
Return type:str
prepare_request_challenge()[source]

Creates prepared request to send challenge

Returns:prepared request
Return type:requests.PreparedRequest
prepare_request_verify()[source]

Creates prepared request to send verification

Returns:prepared request
Return type:requests.PreparedRequest
request(method, url, headers=None, withhold_token=False, **kwargs)[source]

Main request method of the session

Adds authentication to method from requests_toolbelt.BaseUrlSession. Takes auto_refresh_token in mind.

Parameters:withhold_token (dict) – If boolean is True authentication token isn’t added to the request.
Return type:requests.Response
verify_url

Full URL of verify endpoint

Returns:Full URL.
Return type:str
class xled.auth.ChallengeResponseAuth(login_url, verify_url, hw_address=None)[source]

Bases: requests.auth.AuthBase

authenticate(response, **kwargs)[source]

Handles user authentication with challenge-response

deregister(response)[source]

Deregisters the response handler

handle_401(response, **kwargs)[source]

Handles 401’s, attempts to use challenge-response authentication

handle_response(response, **kwargs)[source]

Takes the given response and tries challenge-auth, as needed.

send_challenge(response, challenge)[source]
send_challenge_response(response)[source]
validate_challenge_response()[source]
class xled.auth.ClientApplication(challenge=None)[source]

Bases: xled.auth.ValidatingClientMixin

new_challenge()[source]

Generates a challenge string to be used in authorizations.

parse_response_challenge(response, **kwargs)[source]

Modifies prepared request so challenge can be sent to login

Param:requests.PreparedRequest response prepared request
Returns:Modified prepared request
Return type:requests.PreparedRequest
Raises:AuthenticationError – if application response isn’t valid
parse_response_verify(response, **kwargs)[source]

Process response from verify call

This is last step to be able to use token to authenticate.

Param:requests.Response response Response to process.
Returns:Same response that was used as parameter
Return type:requests.Response
Raises:AuthenticationError – if application response isn’t valid
populate_token_attributes(response)[source]

Fetches token attributes from application response

Param:app_response response Response from login endpoint.
Type:application_response ApplicationResponse
prepare_request_challenge(request)[source]

Modifies prepared request so it can be sent to login

Param:requests.PreparedRequest request prepared request to modify
Returns:Modified prepared request
Return type:requests.PreparedRequest
prepare_request_verify(request)[source]

Modifies prepared request so it can be sent to verify challenge

Param:requests.PreparedRequest request prepared request to modify
Returns:Modified prepared request
Return type:requests.PreparedRequest
token_expired
token_valid
class xled.auth.ValidatingClientMixin[source]

Bases: object

Mixin adds functionality to ClientApplication to authenticate server

challenge_response_valid(hw_address=None)[source]

Verifies server with hardware address returned correct challenge response

Creates challenge-response for server’s hardware address, challenge and shared password and compares it with stored challenge-response.

Parameters:hw_address (str) – Hardware address of a server.
Returns:If challenge-response is valid returns True. If it cannot be verified returns None.
Return type:bool or None
Raises:ValidationError – if chalenge-response is invalid

xled.cli module

Console script for xled.

xled.cli.common_preamble(name=None, host_address=None)[source]
xled.cli.validate_time(ctx, param, value)[source]

xled.compat module

xled.compat

This module handles import compatibility issues between Python 2 and Python 3.

xled.compat.is_py2 = False

Python 2.x?

xled.compat.is_py3 = True

Python 3.x?

xled.control module

xled.control

This module contains interface to control specific device

See also

RESTful API reference
for more details about API that is used by the application.
Protocol details
for various operations.
class xled.control.ControlInterface(host, hw_address=None)[source]

Bases: object

Main interface to control specific device

Parameters:host (str) – Hostname (or IP address) of a device to control
base_url
check_status()[source]

Checks that the device is online and responding

Raises:ApplicationError – on application error
Return type:ApplicationResponse
delete_movies()[source]

Remove all uploaded movies.

Raises:ApplicationError – on application error
Return type:ApplicationResponse
delete_playlist()[source]

Clears the playlist

Raises:ApplicationError – on application error
Return type:ApplicationResponse
firmware_0_update(firmware)[source]

Uploads first stage of the firmware

Parameters:firmware – file-like object that points to firmware file.
Raises:ApplicationError – on application error
Return type:ApplicationResponse
firmware_1_update(firmware)[source]

Uploads second stage of the firmware

Parameters:firmware – file-like object that points to firmware file.
Raises:ApplicationError – on application error
Return type:ApplicationResponse
firmware_update(stage0_sha1sum, stage1_sha1sum=None)[source]

Performs firmware update from previously uploaded images

Parameters:
  • stage0_sha1sum (str) – SHA1 digest of first stage
  • stage1_sha1sum (str) – SHA1 digest of second stage
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

firmware_version()[source]

Gets firmware version

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_brightness()[source]

Gets current brightness level and if dimming is applied

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_device_info()[source]

Gets detailed information about device

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_device_name()[source]

Gets device name.

Raises:ApplicationError – on application error
Returns:current device name.
Return type:ApplicationResponse
get_led_color()[source]

Gets the color used in color mode

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_led_config()[source]

Gets the structural configuration of the leds in term of strings

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_led_effects()[source]

Gets the number of effects and their unique_ids

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_led_effects_current()[source]

Gets the current effect index

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_led_layout()[source]

Gets the physical layout of the leds

See also

set_led_layout()

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_led_movie_config()[source]

Gets the parameters for playing the uploaded movie

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_mode()[source]

Gets current LED operation mode.

See also

set_mode() to set modes.

Raises:ApplicationError – on application error
Returns:current LED operation mode. See set_mode() for possible return values.
Return type:ApplicationResponse
get_movies()[source]

Gets list of uploaded movies.

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_movies_current()[source]

Gets the movie id of the currently played movie in the movie list

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_mqtt_config()[source]

Gets the mqtt configuration parameters

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_network_status()[source]

Gets network status

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_playlist()[source]

Gets the current playlist

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_playlist_current()[source]

Gets the movie id of the currently played movie in the playlist

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_saturation()[source]

Gets current saturation level and if desaturation is applied

Raises:ApplicationError – on application error
Return type:ApplicationResponse
get_timer()[source]

Gets current timer

Raises:ApplicationError – on application error
Returns:{time_on, time_off, time_now}. See set_timer() for explanation of return values.
Return type:ApplicationResponse
led_reset()[source]

Resets LED

Raises:ApplicationError – on application error
Return type:ApplicationResponse
network_scan()[source]

Initiate WiFi network scan

Raises:ApplicationError – on application error
Return type:ApplicationResponse
network_scan_results()[source]

Get results of WiFi network scan

Raises:ApplicationError – on application error
Return type:ApplicationResponse
session

Session object to operate on

Returns:session object with auth BaseUrlChallengeResponseAuthSession().
Return type:requests.Session
set_brightness(brightness=None, enabled=True, relative=False)[source]

Sets new brightness or enable/disable brightness dimming

Parameters:
  • brightness – new brightness in range of 0..100 or a relative change in -100..100 or None if no change is requested
  • enabled (bool) – set to False if no dimming should be applied
  • relative (bool) – set to True to make a relative change
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_device_name(name)[source]

Sets new device name

Parameters:name (str) – new device name
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_led_color_hsv(h, s, v)[source]

Sets the color used in color mode, given as HSV (hue, saturation, value)

Parameters:
  • h (int) – hue component [0, 360]
  • s (int) – saturation component [0, 255]
  • v (int) – value component [0, 255]
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_led_color_rgb(r, g, b)[source]

Sets the color used in color mode, given as RGB

Parameters:
  • r (int) – red component
  • g (int) – green component
  • b (int) – blue component
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_led_effects_current(effect_id)[source]

Sets the current effect of effect mode

Parameters:effect_id (int) – id of effect
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_led_layout(source, coordinates, synthesized=False)[source]

Sets the physical layout of the leds

Parameters:
  • source (str) – 2d, 3d, or linear
  • coordinates (list) – list of dictionaries with keys ‘x’, ‘y’, and ‘z’
  • synthesized (bool) – presumably whether it is synthetic or real coordinates
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_led_movie_config(frame_delay, frames_number, leds_number)[source]

Sets movie configuration for the last uploaded movie

Parameters:
  • frame_delay (int) – speed of movie (delay between frames in ms)
  • leds_number (int) – total number of LEDs
  • frames_number (int) – total number of frames
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_led_movie_full(movie)[source]

Uploads movie

Parameters:movie – file-like object that points to movie file.
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_mode(mode)[source]

Sets new LED operation mode.

Parameters:mode (str) – Mode to set. One of ‘movie’, ‘playlist’, ‘rt’, ‘demo’, ‘effect’, ‘color’ or ‘off’.
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_movies_current(movie_id)[source]

Sets which movie in the movie list to play

Parameters:movie_id (int) – id of movie to play
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_movies_full(movie)[source]

Uploads a movie to the movie list

Presumes that ‘set_movies_new’ has been called earlier with the movie params.

Parameters:movie – file-like object that points to movie file.
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_movies_new(name, uid, dtype, nleds, nframes, fps)[source]

Prepares the upload of a new movie to the movie list by setting its parameters

Parameters:
  • name (str) – name of new movie
  • uid (str) – unique id of new movie
  • dtype (str) – descriptor_type, one of rgb_raw, rgbw_raw, or aww_raw
  • nleds (int) – number of leds
  • nframes (int) – number of frames
  • fps (int) – frames per second of the new movie
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_mqtt_config(broker_host=None, broker_port=None, client_id=None, user=None, interval=None)[source]

Sets the mqtt configuration parameters

Parameters:
  • broker_host (str) – optional broker host
  • broker_port (int) – optional broker port
  • client_id (str) – optional client_id
  • user (str) – optional user name
  • interval (int) – optional keep alive interval
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_network_mode_ap(password=None)[source]

Sets network mode to Access Point

If password is given, changes the Access Point password (after which you have to connect again with the new password)

Parameters:password (str) – new password to set
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_network_mode_station(ssid=None, password=None)[source]

Sets network mode to Station for firmware up until 2.4.22

The first time you need to provide an ssid and password for the WIFI to connect to.

Parameters:
  • ssid (str) – SSID of the access point to connect to
  • password (str) – password to use
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_network_mode_station_v2(ssid=None, password=None)[source]

Sets network mode to Station since firmware version 2.4.30

The first time you need to provide an ssid and password for the WIFI to connect to.

Parameters:
  • ssid (str) – SSID of the access point to connect to
  • password (str) – password to use
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_playlist(entries)[source]

Sets a new playlist

Parameters:entries (list) – list of playlist entries each with keys “unique_id” and “duration”
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_playlist_current(movie_id)[source]

Sets which movie in the playlist to play

Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_rt_frame_rest(frame)[source]

Uploads a frame in rt-mode, using the ordinary restful protocol

Parameters:frame – file-like object that points to frame file.
Raises:ApplicationError – on application error
Return type:ApplicationResponse
set_rt_frame_socket(frame, version, leds_number=None)[source]

Uploads a frame in rt-mode, over an UDP socket. This is much faster than the restful protocol.

Parameters:
  • frame – file-like object representing the frame
  • version – use protocol version 1, 2 or 3
  • leds_number (int) – the number of leds (only used in version 1)
Return type:

None

set_saturation(saturation=None, enabled=True, relative=False)[source]

Sets new saturation or enable/disable desaturation

Parameters:
  • saturation – new saturation in range of 0..100 or a relative change in -100..100 or None if no change is requested
  • enabled (bool) – set to False if no desaturation should be applied
  • relative (bool) – set to True to make a relative change
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

set_timer(time_on, time_off, time_now=None)[source]

Sets new timer

Parameters:
  • time_on (int) – time when to turn lights on. In seconds after midnight. To disable use -1.
  • time_off (int) – time when to turn lights off. In seconds after midnight. To disable use -1.
  • time_now (int or None) – current time in seconds after midnight. Determined automatically if not set.
Raises:

ApplicationError – on application error

Return type:

ApplicationResponse

udpclient

Client for sending UDP packets to the realtime port

Returns:the UDP client UDPClient().
Return type:udp_client.UDPClient
class xled.control.HighControlInterface(host, hw_address=None)[source]

Bases: xled.control.ControlInterface

High level interface to control specific device

disable_timer()[source]

Disables timer

get_formatted_timer()[source]

Gets current time and timer

Returns:namedtuple of formatted entries: current time, turn on time, turn off time.
Return type:namedtuple
is_on()[source]

Returns True if device is on

set_static_color(red, green, blue)[source]

Sets static color for all leds

Parameters:
  • red – integer between 0-255 representing red color
  • green – integer between 0-255 representing green color
  • blue – integer between 0-255 representing blue color
turn_off()[source]

Turns off the device.

turn_on()[source]

Turns on the device.

In the order tries to set mode to: playlist or movie. Once the device’s response is ok, it stops trying.

Raises:HighInterfaceError – if none of mode sets returned ok response
Return type:ApplicationResponse
update_firmware(stage0, stage1)[source]

Uploads firmware and runs update

Parameters:
  • stage0 – file-like object pointing to stage0 of firmware. Must support seek().
  • stage1 – file-like object pointing to stage1 of firmware. Must support seek().
Raises:
static write_static_movie(file_obj, size, red, green, blue)[source]

Writes movie of single color

Parameters:
  • file_obj – file-like object to write movie to.
  • size (int) – numbers of triples (RGB) to write to.
  • red – integer between 0-255 representing red color
  • green – integer between 0-255 representing green color
  • blue – integer between 0-255 representing blue color
xled.control.REALTIME_UDP_PORT_NUMBER = 7777

UDP port to send realtime frames to

xled.control.TIME_FORMAT = '%H:%M:%S'

Time format as defined by C standard

xled.discover module

xled.discover

This module contains interface for discovery devices on the network

class xled.discover.DiscoveryInterface(destination_host=None, receive_timeout=None)[source]

Bases: object

Main interface to discover devices on the network

Starts an UDP ping agent in a background thread automatically after initialisation.

recv()[source]

Receive a message from the interface

stop()[source]

Stop ping agent and close pipe for communication with callee

class xled.discover.InterfaceAgent(ctx, pipe, loop=None, destination_host=None, receive_timeout=None)[source]

Bases: object

This structure holds the context for our agent

This way it can be passed around cleanly to methods that need it.

Parameters:
  • ctxzmq.Context object.
  • pipe – Pipe back to the main thread of to pass messages.
  • loop – (optional) loop to use.
control_message(event)[source]

Respond to control message from main application thread

Currently unused.

Parameters:event – anything.
get_mac_address(ip_address)[source]

Gets the MAC address of the device at ip_address.

Parameters:ip_address – The IP address or hostname to the device
Returns:The MAC address, or None in case of failure
handle_beacon(fd, event)[source]

Reads response from nodes

Creates Peer objects and tracks them in self.peers. Finally sends messages through pipe to main application thread.

Parameters:
  • fd – not used
  • event – not used
peers = None

Hash of known peers, fast lookup

process_new_peer(hw_address, device_id, ip_address)[source]

Adds new peer and sends out status message

This is called when we receive a message from HW address we don’t have in a list of peers. Adds peer info in a list of peers sends out message JOINED message.

Parameters:
  • hw_address (str) – HW address of a device from which we have received a beacon. Must not exist in list of peers.
  • device_id (str) – device ID decoded from a beacon
  • ip_address (str) – IP address decoded from a beacon
process_seen_peer(hw_address, device_id, ip_address)[source]

Updates seen peer’s info and sends out status message

This is called when we receive a message from a peer that we track as seen peers. Updates expiry time for a peer and sends out ALIVE message. If device ID or IP address changed updates peer’s info and sends out message RENAMED or ADDRESS_CHANGED messages respectively.

Parameters:
  • hw_address (str) – HW address of a device from which we have received a beacon. Must exist in list of peers.
  • device_id (str) – device ID decoded from a beacon
  • ip_address (str) – IP address decoded from a beacon
reap_peers()[source]

Removes peers whose activity wasn’t seen for a long time

Called periodically. Sends messages through pipe to main application thread.

send_ping(*args, **kwargs)[source]

Sends ping message

Runs periodically.

start()[source]

Main entry of the thread

Hooks necessary handlers to send pings, process incoming data and mark peers offline if they doesn’t respond for long time.

stop()[source]

Stop the loop of agent

xled.discover.PEER_EXPIRY = 5.0

After how many seconds the device is considered offline

xled.discover.PING_INTERVAL = 1.0

Interval in seconds

xled.discover.PING_MESSAGE = b'\x01discover'

Message to send in ping requests

xled.discover.PING_PORT_NUMBER = 5555

Default port number to send pings

class xled.discover.Peer(hw_address, device_id, ip_address)[source]

Bases: object

Each object of this class represents one device on the network

Parameters:
  • hw_address – Hardware (MAC) address of a device.
  • device_id – Id of the device.
  • ip_address – IP address of a device.
is_alive()[source]

Reset the peers expiry time

Call this method whenever we get any activity from a peer.

xled.discover.decode_discovery_response(data)[source]

Decodes response for discovery

xled.discover.discover(find_id=None, destination_host=None, timeout=None)[source]

Wrapper of xdiscover() to return first entry

xled.discover.pipe(ctx)[source]

Create an inproc PAIR pipe

Used for communicating between parent and children.

Parameters:ctxzmq.Context object.
Returns:parent socket, child socket.
Return type:tuple
xled.discover.xdiscover(find_id=None, destination_host=None, timeout=None)[source]

Generator discover all devices or device of specific id

Device can be specified either by id or by host.

Parameters:
  • find_id (str) – (optional) Device id to look for. If not set first node that responded is returned.
  • destination_host (str) – (optional) Ping selected node only.
  • timeout (float) – (optional) Number of seconds until discovery timeouts.
Returns:

namedtuple of hardware address, device id and host name.

Return type:

namedtuple

Raises:

DiscoverTimeout – timeout exceeded while waiting for a device

xled.exceptions module

exception xled.exceptions.ApplicationError(*args, **kwargs)[source]

Bases: xled.exceptions.XledException

Application didn’t return successful status code

exception xled.exceptions.AuthenticationError(*args, **kwargs)[source]

Bases: xled.exceptions.XledException

Authentication handshake wasn’t successful

exception xled.exceptions.DiscoverTimeout(*args, **kwargs)[source]

Bases: xled.exceptions.XledException

Signal that timeout occurred while discover is looking for a device

exception xled.exceptions.HighInterfaceError(*args, **kwargs)[source]

Bases: xled.exceptions.XledException

High level interface error

exception xled.exceptions.ReceiveTimeout(*args, **kwargs)[source]

Bases: xled.exceptions.XledException

Signal that timeout occurred while waiting for data

exception xled.exceptions.TokenExpiredError(*args, **kwargs)[source]

Bases: xled.exceptions.XledException

Token is no longer valid

exception xled.exceptions.ValidationError(*args, **kwargs)[source]

Bases: xled.exceptions.XledException

Validation of challenge response wasn’t successful

exception xled.exceptions.XledException(*args, **kwargs)[source]

Bases: OSError

xled.response module

class xled.response.ApplicationResponse(response=None)[source]

Bases: collections.abc.Mapping

The ApplicationResponse object, which contains a server’s response to an HTTP request.

Parameters:response (requests.Response or None) – to which this is a response. Can be later set as an attribute.
data

Response content as dict

ok

Returns True if status_code is 1000, False if not.

First this attribute checks if parent response is ok. Then it checks if application response can be determined and finally if status_code is 1000.

raise_for_status(propagate=True)[source]

Raises ApplicationError, if one occurred.

Parameters:propagate (bool) – check status of underlying requests.Response as well.
Raises:ApplicationError – if response cannot be parsed as JSON or application status code wasn’t success (1000).
Return type:None
status_code

Integer Code of responded application status, e.g. 1000 or 1001

xled.response.build_response(response)[source]

Creates ApplicationResponse object out of Requests response

Parameters:response (requests.Response or None) – to which this is a response. Can be later set as an attribute.
Return type:ApplicationResponse

xled.security module

xled.security

This module contains cryptographic functions to encrypt data with shared secret so it can be transferred over unencrypted connection.

See also

Protocol details
for various operations.
xled.security.BUFFER_SIZE = 65536

Read buffer size for sha1sum

xled.security.SHARED_KEY_CHALLANGE = b'evenmoresecret!!'

Default key to encrypt challenge in login phase

xled.security.SHARED_KEY_WIFI = b'supersecretkey!!'

Default key to encrypt WiFi password

xled.security.SHARED_KEY_WIFI_V2 = b'&\x80\xf5\x87\x9f\xee,u\x11\xaa\x08\x15GD\x8e\x04\x99\xcdh\x07n\t2b]\xc4\xde|8\x98\x9e\x88\x80\xee*\xb73g\x8f\xa2\r\xcc\x85\xd8\x94\xcd\x94O'

Default key to encrypt WiFi password and SSID used since firmware v.2.4.25

xled.security.derive_key(shared_key, mac_address)[source]

Derives secret key from shared key and MAC address

MAC address is repeated to length of key. Then bytes on corresponding positions are xor-ed. Finally a string is created.

Parameters:
  • shared_key (str) – secret key
  • mac_address (str) – MAC address in any format that netaddr.EUI recognizes
Returns:

derived key

Return type:

bytes

xled.security.encrypt_wifi_credentials(credential, mac_address, shared_key)[source]

Encrypts WiFi credentials

Derives a secret key out of mac_address and shared_key which is then used to encrypt the credential. This can be used to send password or SSID for WiFi in encrypted form over unencrypted channel.

Parameters:
  • credential (str) – secret in clear text to encrypt
  • mac_address (str) – MAC address of the remote device in AP mode or from gestalt call in any format that netaddr.EUI recognizes
  • shared_key (str) – shared key that device has to know
Returns:

ciphertext encoded as base 64 string

Return type:

str

xled.security.encrypt_wifi_password(password, mac_address, key=b'supersecretkey!!')[source]

Encrypts WiFi password

This can be used to send password for WiFi in encrypted form over unencrypted channel. Ideally only device that knows shared secret key and has defined MAC address should be able to decrypt the message.

This is backward compatible API which wraps encrypt_wifi_credentials(). Predefined key was used to encrypt only password up until firmware version 2.4.22. Since firmware 2.4.25 a different key is used and also the SSID is encrypted. While this function still can be used, its name and arguments might be confusing for readers.

Parameters:
  • password (str) – password to encrypt
  • mac_address (str) – MAC address of the remote device in any format that netaddr.EUI recognizes
  • key (str) – (optional) shared key that device has to know
Returns:

Base 64 encoded string of ciphertext of input password

Return type:

str

xled.security.generate_challenge()[source]

Generates random challenge string

Return type:str
xled.security.make_challenge_response(challenge_message, mac_address, key=b'evenmoresecret!!')[source]

Create challenge response from challenge

Used in initial login phase of communication with device. Could be used to check that device shares same shared secret and implements same algorithm to show that it is genuine.

Parameters:
  • challenge_message (str) – random message originally sent as challenge with login request
  • mac_address (str) – MAC address of the remote device in any format that netaddr.EUI recognizes
  • key (str) – (optional) shared key that device has to know
Returns:

hashed ciphertext that must be equal to challenge-response in response to login call

Return type:

str

xled.security.rc4(message, key)[source]

Simple wrapper for RC4 cipher that encrypts message with key

Parameters:
  • message (str) – input to encrypt
  • key (str) – encryption key
Returns:

ciphertext

Return type:

str

xled.security.sha1sum(fileobj)[source]

Computes SHA1 from file-like object

It is up to caller to open file for reading and close it afterwards.

Parameters:fileobj – file-like object
Returns:SHA1 digest as hexdecimal digits only
Return type:str
xled.security.xor_strings(message, key)[source]

Apply XOR operation on every corresponding byte

If key is shorter than message repeats it from the beginning until whole message is processed. :param bytes message: input message to encrypt :param bytes key: encryption key :return: encrypted cypher :rtype: bytearray

xled.udp_client module

xled.udp_client

A Simple UDP class

class xled.udp_client.UDPClient(port, destination_host=None, broadcast=False, receive_timeout=None)[source]

Bases: object

Creates simple UDP client

Object can be used either to send to broadcast or unicast address.

Parameters:
  • port (int) – destination port to connect to and from which received packets will be read.
  • destination_host (str or None) – unicast IP address to send packets to. If broadcast parameter is set to True and this parameter is left to None DEFAULT_BROADCAST is used automatically.
  • broadcast (bool) – use broadcast for a socket
close()[source]

Closes socket handler

handle

Socket handler for send/recv

recv(bufsize)[source]

Blocks until message is received

Parameters:bufsize (int) – the maximum amount of data to be received at once
Returns:received message, sender address
Return type:tuple
send(message)[source]

Send a message

Parameters:message (str) – message to send
Returns:number of bytes sent
Return type:int

xled.util module

xled.util

Miscellaneous utility functions.

xled.util.date_from_seconds_after_midnight(seconds)[source]
xled.util.seconds_after_midnight()[source]
xled.util.seconds_after_midnight_from_time(hours, minutes)[source]

Module contents

xled package

xled is a library to control Twinkly LED lights. Basic usage:

>>> import xled
>>> control = xled.HighControlInterface('192.168.4.1')
>>> control.set_mode('demo')
>>> control.turn_off()

The other API calls are supported - see xled.control. Full documentation is at <http://xled.readthedocs.io/>.

copyright:
  1. 2017 by Pavol Babinčák
license:

MIT, see LICENSE for more details.

The Contributor Guide

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

You can contribute in many ways:

Types of Contributions

Report Bugs

Report bugs at https://github.com/scrool/xled/issues. Fill all sections for issue template.

Fix Bugs

Look through the GitHub issues for bugs. Anything tagged with “bug” and “help wanted” is open to whoever wants to implement it.

Generally project should implement only features described on https://xled-docs.readthedocs.io/ . Make sure you update device features, behavior, endpoints and so on there as well.

Write Documentation

xled could always use more documentation, whether as part of the official xled docs, in docstrings, or even on the web in blog posts, articles, and such.

Submit Feedback

The best way to send feedback is to file an issue at https://github.com/scrool/xled/issues.

If you are proposing a feature:

  • Explain in detail how it would work.
  • Keep the scope as narrow as possible, to make it easier to implement.
  • Remember that this is a volunteer-driven project, and that contributions are welcome :)

Get Started!

Ready to contribute? Here’s how to set up xled for local development.

  1. Fork the xled repo on GitHub.

  2. Clone your fork locally:

    $ git clone git@github.com:your_name_here/xled.git
    
  3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:

    $ mkvirtualenv xled
    $ cd xled/
    $ pip install -r requirements_dev.txt
    $ pre-commit install
    $ python setup.py develop
    
  4. Create a branch for local development:

    $ git checkout -b name-of-your-bugfix-or-feature
    

    Now you can make your changes locally.

  5. When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:

    $ flake8 xled tests
    $ python setup.py test or py.test
    $ tox
    

    To get flake8 and tox, just pip install them into your virtualenv.

  6. Commit your changes and push your branch to GitHub:

    $ git add .
    $ git commit -m "Your detailed description of your changes."
    $ git push origin name-of-your-bugfix-or-feature
    
  7. Submit a pull request through the GitHub website.

Pull Request Guidelines

Before you submit a pull request, check that it meets these guidelines:

  1. The pull request should include tests.
  2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
  3. The pull request should work for Python 2.7, 3.7, 3.8, 3.9, 3.10 and 3.11. Check https://github.com/scrool/xled/actions and make sure that the tests pass for all supported Python versions.

Tips

To run a subset of tests:

$ pytest tests/test_security.py -k test_identity

To verify test against your device first remove existing Cassette, e.g.:

$ rm tests/cassettes/TestControlInterface.test_timer.yaml

and then pass IP address of your device with a flag to record:

$ env XLED_TEST_HOST=192.168.1.123 XLED_TEST_IS_RECORDING=True pytest -v tests/test_control_low.py -k test_timer

Credits

Development Lead

Contributors

  • Paul Webster (@PaulWebster)
  • Artem Ignatyev (@timon)
  • Anders Holst (@Anders-Holst)

History

0.7.0 (2021-11-28)

  • Major changes:
    • Add realtime UDP protocol including unit tests
    • Add several missing rest calls in ControlInterface
    • Unit test all methods in ControlInterface
  • Other bugfixes and improvements:
    • Provide a short guide how to install packages from PyPI
    • Provide python_requires in setup.py
    • Add project URLs to metadata
    • Corrected import of security, and removed some old comments
    • Make encrypt_wifi_password work also with python3
    • More flexible parameters to set_mqtt_config
    • Enable options in set_network_mode_ap and _station
    • Enable relative values in set_brightness
    • Make firmware_update compatible with Generation II
    • Fix error in python setup.py install (fix #82)
    • Use generic Exception in discover module
    • On Python 2.7 ignore VCR deprecation warning
    • On Python 2.7 ignore cryptography deprecation warning
    • Fix dependencies for python 2.7
    • Don’t debug log reapped devices
    • Time format as hardcoded value instead of locale specific
    • Raise an exception with a error message firmware update failed
    • Get MAC address from gestalt API call
    • Always UTF-8 decode response from JOINED event in discovery
    • Log instead of print in discovery interface and return on unknown event
    • If hw_address wasn’t possible to resolve don’t use None as a peer
    • Configure all loggers used by CLI with cli_log.basic_config()
    • Make response assertions less strict
    • Reformat setup py so tox tests pass
  • Documentation updates:
    • Update example in README to use reflect change in API
    • Add Gitter badge
    • Update of xled and xled-docs should be done hand in hand
    • Remove Enhancement section from Contributing as there is no such thing
    • Write down support for OS, devices, python and guide to CLI
    • Rewrite README file
    • Fix documentation for set_led_movie_config
  • Changes in CI/CD:
    • Run linters as GitHub action
    • Use generic python3 in black pre-commit config
    • Configure pytest to collect tests only from tests/
    • Use GitHub action for PyPI publish
    • Update URL for CI from Travis to GitHub actions
    • One more place to update supported python versions
    • Make Travis environment python again
    • Remove non-deploy section from travis.yml
    • Fix typo in travis.yml dep install
    • Ignore Flake8 error on Sphinx configuration file
    • Run pytest directly from Tox
    • Add bug report template for GitHub issues and reference it
    • Switch to token authentication for deployment to pypi through Travis
  • Changes in dependencies and python versions:
    • Add 3.10 to supported Python versions
    • Update coverage from 4.4.2 to 5.5
    • Update pip from 20.2.3 to 21.1
    • Update travis.yaml: remove python 3.5 and add 3.8 and 3.9
    • Add 3.9 to supported Python versions
    • Drop Python 3.5 support
    • Drop compatibility code for Python version 3.4
    • Add Python 3.8 as a supported language
    • Update pip from 19.0.3 to 20.2.3
    • Update sphinx from 1.6.5 to 3.0.4

0.6.1 (2020-01-17)

  • Make tests with tox pass again so release can be automatically deployed:
    • Add Black reformatter to tox linter envs
    • Tox config: new linters env to run Flake8
    • Tox config update: Flake8 against tests/ and setup.py as well
    • Make xled.compat pass Flake8 for F821 undefined names
    • Refactor beacon processing of seen/new peer into separate methods
    • Reformat test_control test with black
    • Make tox install test-only requires
    • Use conditional deployment to pypi with travis only from master

0.6.0 (2020-01-15)

  • Drop support for python 3.4
  • Explicitly specify Linux as only operating system
  • Automatically refresh token if expired
  • Add brightness management
  • Check response is OK before trying to decode JSON from body
  • Use id instead of name in discovery
  • Device class representing the device
  • Get network status in control interface
  • Use response from alive device to check if we reached discover timeout
  • Provide generator xdiscover() to return all or specific devices
  • Support timeout for discovery
  • When agent stops stop ping task and processing responses
  • Provide close() for UDPClient and use it on DiscoveryInterface.stop()
  • Do not continue receiving more data if UDP recv timeouts
  • Other bugfixes and improvements:
    • Fix assertions
    • Expose HighControlInterface on package level
    • If ApplicationError is raised, store value of response attribute
    • Allow disable/enable of brightness without value change
    • Update wheel from 0.30.0 to 0.33.1
    • Update pip from 9.0.1 to 19.0.3
    • Add python 3.6 and 3.7 to Travis config

0.5.0 (2018-12-09)

  • CLI to update firmware
  • Example of library call and CLI usage
  • Option to select device by hostname in CLI and ping in discovery
  • New HighControlInterface() to aggregate and abstract low-level calls
  • CLI and HighControlInterface way to set static single color
  • Other bugfixes and improvements:
    • Fix typo in CLI error message
    • Print message before discovery on CLI
    • Refactor: join consecutive strings on same line
    • Print better message after device has been discovered over CLI
    • Regenerate documentation index of a package
    • Fix typo in control.set_mode() documentation
    • Return named tuple in discover.discover()
    • Use discovery and named tuple in example of library use
    • Do not assert return value in ControlInterface.set_led_movie_full()
    • Return ApplicationResponse for ControlInterface.set_led_movie_config()
    • Return ApplicationResponse for control.ControlInterface.led_reset()
    • Remove unneeded debug message from DiscoveryInterface.__init__()

0.4.0 (2018-12-03)

  • Support Python 3.6 and 3.7 including tests and documentation
  • Python 3 support with pyzmq >= 17.0 and Tornado 5
  • Remove redundant udplib
  • Other Python 3 compatibility:
    • In Python 3+ import Mapping from collections.abc
    • Python 3 compatible encoding of discovered IP and HW address and name
    • Make xled.security.xor_strings() compatible with Python 2 and 3
    • Treat PING_MESSAGE as bytes to simplify handling Python 2 and 3
  • Other bugfixes and improvements:
    • Remove mention of PyPy from docs as it wasn’t ever tested on it
    • Improve robustness with sending messages from agent to interface
    • Escape display of binary challenge in debug log of xled.auth
    • Ignore (usually own) PING_MESSAGE on network when handling responses

0.3.1 (2018-11-27)

  • Update changelog for version 0.3.0
  • Update description in setup.py to refer to CLI
  • Fix JSON payload sent to server for firmware update.

0.3.0 (2018-11-27)

  • CLI interface
  • Discovery interface - currently works only on Python 2
  • Add support for API led/movie/full and corresponding CLI upload-movie
  • New Authentication mechanism - use session
  • Rename authentication module from long challenge_response_auth to auth
  • Change interface of ApplicationResponse to collections.Mapping
  • Python files reformatted with Black
  • Other bugfixes and improvements:
    • Really show ApplicationResponse status in repr() when available
    • Catch JSONDecodeError in Python 3.5+ in ApplicationResponse
    • New shortcut method ok() of ApplicationResponse
    • Make ApplicationResponse’s attribute status_code @property
    • Improve error reporting during parsing of ApplicationResponse
    • If repr() of ApplicationResponse is called parse response first
    • Check status of underlying requests’ Response if requested
    • Accept requests’ response as attribute to class ApplicationResponse
    • Move generate_challenge to security module
    • Unit tests for control interface
    • Run unit tests on supported python versions with tox and Travis
    • Configuration for pre-commit-hooks
    • Initial pyup configuration
    • Don’t run Tox on Travis on Python 3.3
    • Update coverage

0.2.1 (2018-01-02)

  • Add missing MANIFEST.in
  • Configure Travis for automatic deployment to PyPI

0.2.0 (2018-01-02)

  • First Python control interface.

0.1.0 (2017-12-17)

  • Low level control interface.

Indices and tables