The lights daemon protocol

The lightsd protocol is implemented on top of JSON-RPC 2.0. This section covers the available methods and how to target bulbs.

Since lightsd implements JSON-RPC without any kind of framing like it usually is the case (using HTTP), this section also explains how to implement your own lightsd client in Writing a client for lightsd.

Targeting bulbs

Commands that manipulate bulbs will take a target argument to define on which bulb(s) the operation should apply. The target argument is either a string (identifying a target as explained in the following table), or an array of strings (targets).

* targets all bulbs
#TagName targets bulbs tagged with TagName
124f31a5 directly target the bulb with the given id (that’s the bulb mac address, see below)
label directly target the bulb with the given Label
[#TagName, 123f31a5] composite target (JSON array)

The mac address (id) of each bulb can be found with get_light_state under the _lifx map, e.g:

"_lifx": {
    "addr": "d0:73:d5:02:e5:30",
    "gateway": {
        […]

This bulb has id d073d502e530.

Note

The maximum supported length for labels and tag names by LIFX bulbs is 32. Anything beyond that will be ignored.

Available methods

power_off(target)

Power off the given bulb(s).

power_on(target)

Power on the given bulb(s).

power_toggle(target)

Power on (if they are off) or power off (if they are on) the given bulb(s).

set_light_from_hsbk(target, h, s, b, k, t)
Parameters:
  • h (float) – Hue from 0 to 360.
  • s (float) – Saturation from 0 to 1.
  • b (float) – Brightness from 0 to 1.
  • k (int) – Temperature in Kelvin from 2500 to 9000.
  • t (int) – Transition duration to this color in ms.
set_waveform(target, waveform, h, s, b, k, period, cycles, skew_ratio, transient)
Parameters:
  • waveform (string) – One of SAW, SINE, HALF_SINE, TRIANGLE, SQUARE.
  • h (float) – Hue from 0 to 360.
  • s (float) – Saturation from 0 to 1.
  • b (float) – Brightness from 0 to 1.
  • k (int) – Temperature in Kelvin from 2500 to 9000.
  • period (int) – milliseconds per cycle.
  • cycles (int) – number of cycles.
  • skew_ratio (float) – from 0 to 1.
  • transient (bool) – if true the target will keep the color it has at the end of the waveform, otherwise it will revert back to its original state.

The meaning of the skew_ratio argument depends on the type of waveform:

SAW Should be 0.5.
SINE Defines the peak point of the function, 0.5 gives you a sine and 1 or 0 will give you cosine. Ignored by firmware 1.1.
HALF_SINE Should be 0.5.
TRIANGLE Defines the peak point of the function like SINE. Ignored by firmware 1.1.
SQUARE Ratio of a cycle the targets are set to the given color.
get_light_state(target)

Return a list of dictionnaries, each dict representing the state of one targeted bulb, the list is not in any specific order. Each dict has the following fields:

  • hsbk: tuple (h, s, b, k) see function:set_light_from_hsbk;
  • label: bulb label (utf-8 encoded string);
  • power: boolean, true when the bulb is powered on false otherwise;
  • tags: list of tags applied to the bulb (utf-8 encoded strings).
set_label(target, label)

Label the target bulb(s) with the given label.

Note

Use tag() instead set_label to give a common name to multiple bulbs.

tag(target, label)

Tag (group) the given target bulb(s) with the given label (group name), then label can be used as a target by prefixing it with #.

To add a device to an existing “group” simply do:

tag(["#myexistingtag", "bulbtoadd"], "myexistingtag")

Note

Notice how # is prepended to the tag label depending on whether it’s used as a target or a regular argument.

untag(target, label)

Remove the given tag from the given target bulb(s). To completely delete a tag (group), simple do:

untag("#myexistingtag", "myexistingtag")

Writing a client for lightsd

lightsd does JSON-RPC directly over TCP, requests and responses aren’t framed in any way like it is usually done by using HTTP.

This means that you will very likely need to write a JSON-RPC client specifically for lightsd. You’re actually encouraged to do that as lightsd will probably augment JSON-RPC via lightsd specific JSON-RPC extensions in the future.

JSON-RPC over TCP

JSON-RPC works in a request/response fashion: the socket (network connection) is never used in a full-duplex fashion (data never flows in both direction at the same time):

  1. Write (send) a request on the socket;
  2. Read (receive) the response on the socket;
  3. Repeat.

Writing the request is easy: do successive write (send) calls until you have successfully sent the whole request. The next step (reading/receiving) is a bit more complex. And that said, if the response isn’t useful to you, you can ask lightsd to omit it by turning your request into a notification: if you remove the JSON-RPC id, then you can just send your requests (now notifications) on the socket in a fire and forget fashion.

Otherwise to successfully read and decode JSON-RPC over TCP you will need to implement your own read loop, the algorithm follows. It focuses on the low-level details, adapt it for the language and platform you are using:

  1. Prepare an empty buffer that you can grow, we will accumulate received data in it;
  2. Start an infinite loop and start a read (receive) for a chunk of data (e.g: 4KiB), accumulate the received data in the previous buffer, then try to interpret the data as JSON:
    • if valid JSON can be decoded then break out of the loop;
    • else data is missing and continue the loop;
  3. Decode the JSON data.

Here is a complete Python 3 request/response example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import json
import socket
import uuid

READ_SIZE = 4096
ENCODING = "utf-8"

# Connect to lightsd, here using an Unix socket. The rest of the example is
# valid for TCP sockets too. Replace /run/lightsd/socket by the output of:
# echo $(lightsd --rundir)/socket
lightsd_socket = socket.socket(socket.AF_UNIX)
lightsd_socket.connect("/run/lightsd/socket")
lightsd_socket.settimeout(2)  # seconds

# Prepare the request:
request = json.dumps({
    "method": "get_light_state",
    "params": ["*"],
    "jsonrpc": "2.0",
    "id": str(uuid.uuid4()),
}).encode(ENCODING, "surrogateescape")

# Send it:
lightsd_socket.sendall(request)

# Prepare an empty buffer to accumulate the received data:
response = bytearray()
while True:
    # Read a chunk of data, and accumulate it in the response buffer:
    response += lightsd_socket.recv(READ_SIZE)
    try:
        # Try to load the received the data, we ignore encoding errors
        # since we only wanna know if the received data is complete.
        json.loads(response.decode(ENCODING, "ignore"))
        break  # Decoding was successful, we have received everything.
    except Exception:
        continue  # Decoding failed, data must be missing.

response = response.decode(ENCODING, "surrogateescape")
print(json.loads(response))

Notes

  • Use an incremental JSON parser if you have one handy: for responses multiple times the size of your receive window it will let you avoid decoding the whole response at each iteration of the read loop;
  • lightsd supports batch JSON-RPC requests, use them!