changeset 228:a655a3b88d62

finish the mq
author Louis Opter <kalessin@kalessin.fr>
date Sat, 08 Aug 2015 02:36:59 -0700
parents 0f355a601f13
children 1a2bd9fc41b1
files add_command_pipe.patch add_daemon_module.patch add_tag_and_untag.patch fix_crash_in_get_light_state_when_tag_does_not_exist.patch fix_lifx_wire_float_endian_functions_naming.patch fix_set_power_state.patch fix_targeting_on_big_endian_architectures.patch fix_unused_unused_attribute.patch fix_usage_and_version.patch ignore_duplicate_listening_address.patch ignore_pcaps.patch pass_pkt_info_around.patch relax_timings.patch remove_assert_on_request_id.patch series update_readme.patch
diffstat 16 files changed, 0 insertions(+), 8339 deletions(-) [+]
line wrap: on
line diff
--- a/add_command_pipe.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2506 +0,0 @@
-# HG changeset patch
-# Parent  51336c56c6a8d78e341c3aef261c9014d41823eb
-Add support for a write-only command pipe for easy scripting
-
-You can just dump simple JSON-RPC calls in it, it will pair nicely with
-the toggle command that I'm gonna implement.
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -42,6 +42,8 @@
-     "-D_POSIX_C_SOURCE=200809L"
-     "-D_BSD_SOURCE=1"
-     "-D_DEFAULT_SOURCE=1"
-+
-+    "-D_DARWIN_C_SOURCE=1"
- )
- 
- IF (CMAKE_BUILD_TYPE MATCHES "DEBUG")
-diff --git a/README.rst b/README.rst
---- a/README.rst
-+++ b/README.rst
-@@ -33,8 +33,8 @@
-   tests);
- - toggle (power on if off and vice-versa, coming up).
- 
--The JSON-RPC interface works on top on IPv4/v6, over a command (named) pipe
--(coming up) and Unix sockets (coming up).
-+The JSON-RPC interface works on top of TCP/IPv4/v6, Unix sockets (coming up) or
-+over a command pipe (named pipe, see mkfifo(1)).
- 
- lightsd can target single or multiple bulbs at once:
- 
-diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
---- a/core/CMakeLists.txt
-+++ b/core/CMakeLists.txt
-@@ -20,6 +20,7 @@
-     listen.c
-     lightsd.c
-     log.c
-+    pipe.c
-     proto.c
-     router.c
- )
-diff --git a/core/client.c b/core/client.c
---- a/core/client.c
-+++ b/core/client.c
-@@ -23,6 +23,7 @@
- #include <stdbool.h>
- #include <stdlib.h>
- #include <stdint.h>
-+#include <string.h>
- 
- #include <event2/buffer.h>
- #include <event2/bufferevent.h>
-@@ -138,6 +139,29 @@
- }
- 
- void
-+lgtd_client_write_string(struct lgtd_client *client, const char *msg)
-+{
-+    assert(client);
-+    assert(msg);
-+
-+    if (client->io) {
-+        bufferevent_write(client->io, msg, strlen(msg));
-+    }
-+}
-+
-+void
-+lgtd_client_write_buf(struct lgtd_client *client, const char *buf, int bufsz)
-+{
-+    assert(client);
-+    assert(buf);
-+    assert(bufsz >= 0);
-+
-+    if (bufsz > 0 && client->io) {
-+        bufferevent_write(client->io, buf, bufsz);
-+    }
-+}
-+
-+void
- lgtd_client_send_response(struct lgtd_client *client, const char *msg)
- {
-     lgtd_jsonrpc_send_response(client, msg);
-@@ -195,3 +219,13 @@
- 
-     return client;
- }
-+
-+void
-+lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-+{
-+    assert(pipe_client);
-+
-+    memset(pipe_client, 0, sizeof(*pipe_client));
-+
-+    jsmn_init(&pipe_client->jsmn_ctx);
-+}
-diff --git a/core/client.h b/core/client.h
---- a/core/client.h
-+++ b/core/client.h
-@@ -42,13 +42,12 @@
- };
- LIST_HEAD(lgtd_client_list, lgtd_client);
- 
--#define LGTD_CLIENT_WRITE_STRING(client, s) do {        \
--    bufferevent_write((client)->io, s, strlen((s)));    \
--} while(0)
--
- struct lgtd_client *lgtd_client_open(evutil_socket_t, const struct sockaddr_storage *);
-+void lgtd_client_open_from_pipe(struct lgtd_client *);
- void lgtd_client_close_all(void);
- 
-+void lgtd_client_write_string(struct lgtd_client *, const char *);
-+void lgtd_client_write_buf(struct lgtd_client *, const char *, int);
- void lgtd_client_send_response(struct lgtd_client *, const char *);
- void lgtd_client_start_send_response(struct lgtd_client *);
- void lgtd_client_end_send_response(struct lgtd_client *);
-diff --git a/core/jsonrpc.c b/core/jsonrpc.c
---- a/core/jsonrpc.c
-+++ b/core/jsonrpc.c
-@@ -480,7 +480,7 @@
- lgtd_jsonrpc_write_id(struct lgtd_client *client)
- {
-     if (!client->current_request->id) {
--        LGTD_CLIENT_WRITE_STRING(client, "null");
-+        lgtd_client_write_string(client, "null");
-         return;
-     }
- 
-@@ -492,7 +492,7 @@
-         start = client->current_request->id->start;
-         stop = client->current_request->id->end;
-     }
--    bufferevent_write(client->io, &client->json[start], stop - start);
-+    lgtd_client_write_buf(client, &client->json[start], stop - start);
- }
- 
- void
-@@ -503,15 +503,15 @@
-     assert(client);
-     assert(message);
- 
--    LGTD_CLIENT_WRITE_STRING(client, "{\"jsonrpc\": \"2.0\", \"id\": ");
-+    lgtd_client_write_string(client, "{\"jsonrpc\": \"2.0\", \"id\": ");
-     lgtd_jsonrpc_write_id(client);
--    LGTD_CLIENT_WRITE_STRING(client, ", \"error\": {\"code\": ");
-+    lgtd_client_write_string(client, ", \"error\": {\"code\": ");
-     char str_code[8] = { 0 };
-     snprintf(str_code, sizeof(str_code), "%d", code);
--    LGTD_CLIENT_WRITE_STRING(client, str_code);
--    LGTD_CLIENT_WRITE_STRING(client, ", \"message\": \"");
--    LGTD_CLIENT_WRITE_STRING(client, message);
--    LGTD_CLIENT_WRITE_STRING(client, "\"}}");
-+    lgtd_client_write_string(client, str_code);
-+    lgtd_client_write_string(client, ", \"message\": \"");
-+    lgtd_client_write_string(client, message);
-+    lgtd_client_write_string(client, "\"}}");
- }
- 
- void
-@@ -521,11 +521,11 @@
-     assert(client);
-     assert(result);
- 
--    LGTD_CLIENT_WRITE_STRING(client, "{\"jsonrpc\": \"2.0\", \"id\": ");
-+    lgtd_client_write_string(client, "{\"jsonrpc\": \"2.0\", \"id\": ");
-     lgtd_jsonrpc_write_id(client);
--    LGTD_CLIENT_WRITE_STRING(client, ", \"result\": ");
--    LGTD_CLIENT_WRITE_STRING(client, result);
--    LGTD_CLIENT_WRITE_STRING(client, "}");
-+    lgtd_client_write_string(client, ", \"result\": ");
-+    lgtd_client_write_string(client, result);
-+    lgtd_client_write_string(client, "}");
- }
- 
- void
-@@ -533,15 +533,15 @@
- {
-     assert(client);
- 
--    LGTD_CLIENT_WRITE_STRING(client, "{\"jsonrpc\": \"2.0\", \"id\": ");
-+    lgtd_client_write_string(client, "{\"jsonrpc\": \"2.0\", \"id\": ");
-     lgtd_jsonrpc_write_id(client);
--    LGTD_CLIENT_WRITE_STRING(client, ", \"result\": ");
-+    lgtd_client_write_string(client, ", \"result\": ");
- }
- 
- void
- lgtd_jsonrpc_end_send_response(struct lgtd_client *client)
- {
--    LGTD_CLIENT_WRITE_STRING(client, "}");
-+    lgtd_client_write_string(client, "}");
- }
- 
- static bool
-diff --git a/core/lightsd.c b/core/lightsd.c
---- a/core/lightsd.c
-+++ b/core/lightsd.c
-@@ -45,6 +45,7 @@
- #include "jsmn.h"
- #include "jsonrpc.h"
- #include "client.h"
-+#include "pipe.h"
- #include "listen.h"
- #include "lightsd.h"
- 
-@@ -60,6 +61,7 @@
- lgtd_cleanup(void)
- {
-     lgtd_listen_close_all();
-+    lgtd_command_pipe_close_all();
-     lgtd_client_close_all();
-     lgtd_lifx_timer_close();
-     lgtd_lifx_broadcast_close();
-@@ -126,8 +128,14 @@
- lgtd_usage(const char *progname)
- {
-     printf(
--        "Usage: %s -l addr:port [-l ...] [-f] [-t] [-h] [-V] "
--        "[-v debug|info|warning|error]\n",
-+        "Usage: %s ...\n\n"
-+        "  [-l,--listen addr:port [+]]\n"
-+        "  [-c,--comand-pipe /command/fifo [+]]\n"
-+        "  [-f,--foreground]\n"
-+        "  [-t,--no-timestamps]\n"
-+        "  [-h,--help]\n"
-+        "  [-V,--version]\n"
-+        "  [-v,--verbosity debug|info|warning|error]\n",
-         progname
-     );
-     exit(0);
-@@ -141,6 +149,7 @@
- 
-     static const struct option long_opts[] = {
-         {"listen",          required_argument, NULL, 'l'},
-+        {"command-pipe",    required_argument, NULL, 'c'},
-         {"foreground",      no_argument,       NULL, 'f'},
-         {"no-timestamps",   no_argument,       NULL, 't'},
-         {"help",            no_argument,       NULL, 'h'},
-@@ -148,7 +157,7 @@
-         {"version",         no_argument,       NULL, 'V'},
-         {NULL,              0,                 NULL, 0}
-     };
--    const char short_opts[] = "l:fthv:V";
-+    const char short_opts[] = "l:c:fthv:V";
- 
-     for (int rv = getopt_long(argc, argv, short_opts, long_opts, NULL);
-          rv != -1;
-@@ -164,6 +173,12 @@
-             if (!lgtd_listen_open(optarg, sep + 1)) {
-                 exit(1);
-             }
-+            break;
-+        case 'c':
-+            if (!lgtd_command_pipe_open(optarg)) {
-+                exit(1);
-+            }
-+            break;
-         case 'f':
-             lgtd_opts.foreground = true;
-             break;
-diff --git a/core/pipe.c b/core/pipe.c
-new file mode 100644
---- /dev/null
-+++ b/core/pipe.c
-@@ -0,0 +1,248 @@
-+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
-+//
-+// This file is part of lighstd.
-+//
-+// lighstd is free software: you can redistribute it and/or modify
-+// it under the terms of the GNU General Public License as published by
-+// the Free Software Foundation, either version 3 of the License, or
-+// (at your option) any later version.
-+//
-+// lighstd is distributed in the hope that it will be useful,
-+// but WITHOUT ANY WARRANTY; without even the implied warranty of
-+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-+// GNU General Public License for more details.
-+//
-+// You should have received a copy of the GNU General Public License
-+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
-+
-+#include <sys/queue.h>
-+#include <sys/types.h>
-+#include <sys/stat.h>
-+#include <assert.h>
-+#include <err.h>
-+#include <errno.h>
-+#include <fcntl.h>
-+#include <stdarg.h>
-+#include <stdbool.h>
-+#include <stdlib.h>
-+#include <string.h>
-+#include <unistd.h>
-+
-+#include <event2/buffer.h>
-+#include <event2/event.h>
-+
-+#include "jsmn.h"
-+#include "jsonrpc.h"
-+#include "client.h"
-+#include "pipe.h"
-+#include "lightsd.h"
-+
-+struct lgtd_command_pipe_list lgtd_command_pipes =
-+    SLIST_HEAD_INITIALIZER(&lgtd_command_pipes);
-+
-+static void
-+lgtd_command_pipe_close(struct lgtd_command_pipe *pipe)
-+{
-+    assert(pipe);
-+
-+    event_del(pipe->read_ev);
-+    if (pipe->fd != -1) {
-+        close(pipe->fd);
-+    }
-+    unlink(pipe->path);
-+    SLIST_REMOVE(&lgtd_command_pipes, pipe, lgtd_command_pipe, link);
-+    evbuffer_free(pipe->read_buf);
-+    event_free(pipe->read_ev);
-+
-+    lgtd_info("closed command pipe %s", pipe->path);
-+    free(pipe);
-+}
-+
-+static void
-+lgtd_command_pipe_read_callback(evutil_socket_t socket, short events, void *ctx)
-+{
-+    assert(ctx);
-+    assert(socket != -1);
-+
-+    (void)socket;
-+    (void)events;
-+
-+    struct lgtd_command_pipe *pipe = ctx;
-+
-+    bool drain = false;
-+    int read = 0;
-+    for (int nbytes = evbuffer_read(pipe->read_buf, pipe->fd, -1);
-+         nbytes;
-+         nbytes = evbuffer_read(pipe->read_buf, pipe->fd, -1)) {
-+        if (nbytes == -1) {
-+            if (errno == EINTR) {
-+                continue;
-+            }
-+            if (errno != EAGAIN) {
-+                lgtd_warn("read error on command pipe %s", pipe->path);
-+                const char *path = pipe->path;
-+                lgtd_command_pipe_close(pipe);
-+                lgtd_command_pipe_open(path);
-+                return;
-+            }
-+            continue;
-+        }
-+
-+        if (!drain) {
-+            read += nbytes;
-+        next_request:
-+            (void)0;
-+            const char *buf = (char *)evbuffer_pullup(pipe->read_buf, -1);
-+            ssize_t bufsz = evbuffer_get_length(pipe->read_buf);
-+            jsmnerr_t rv = jsmn_parse(
-+                &pipe->client.jsmn_ctx,
-+                buf,
-+                bufsz,
-+                pipe->client.jsmn_tokens,
-+                LGTD_ARRAY_SIZE(pipe->client.jsmn_tokens)
-+            );
-+            switch (rv) {
-+            case JSMN_ERROR_NOMEM:
-+            case JSMN_ERROR_INVAL:
-+                lgtd_warnx("pipe %s: request too big or invalid", pipe->path);
-+                // ignore what's left
-+                drain = true;
-+                break;
-+            case JSMN_ERROR_PART:
-+#pragma GCC diagnostic push
-+#pragma GCC diagnostic ignored "-Wswitch"
-+            case 0:
-+#pragma GCC diagnostic pop
-+                if (bufsz >= LGTD_CLIENT_MAX_REQUEST_BUF_SIZE) {
-+                    lgtd_warnx("pipe %s: request too big", pipe->path);
-+                    drain = true;
-+                }
-+                break;
-+            default:
-+                pipe->client.json = buf;
-+                int ntokens = rv;
-+                lgtd_jsonrpc_dispatch_request(&pipe->client, ntokens);
-+
-+                pipe->client.json = NULL;
-+                jsmn_init(&pipe->client.jsmn_ctx);
-+                int request_size = pipe->client.jsmn_tokens[0].end;
-+                evbuffer_drain(pipe->read_buf, request_size);
-+                read -= request_size;
-+                if (read) {
-+                    goto next_request;
-+                }
-+                break;
-+            }
-+        } else {
-+            evbuffer_drain(pipe->read_buf, read + nbytes);
-+            read = 0;
-+        }
-+    }
-+
-+    if (read) {
-+        lgtd_debug(
-+            "pipe %s: discarding %d bytes of unusable data", pipe->path, read
-+        );
-+        evbuffer_drain(pipe->read_buf, read);
-+    }
-+    jsmn_init(&pipe->client.jsmn_ctx);
-+}
-+
-+static mode_t
-+lgtd_command_pipe_get_umask(void)
-+{
-+    mode_t mask = umask(0);
-+    umask(mask);
-+    return mask;
-+}
-+
-+bool
-+lgtd_command_pipe_open(const char *path)
-+{
-+    assert(path);
-+
-+    struct lgtd_command_pipe *pipe;
-+    SLIST_FOREACH(pipe, &lgtd_command_pipes, link) {
-+        if (!strcmp(pipe->path, path)) {
-+            return true;
-+        }
-+    }
-+
-+    pipe = calloc(1, sizeof(*pipe));
-+    if (!pipe) {
-+        lgtd_warn("can't open command pipe %s", path);
-+        return false;
-+    }
-+
-+    lgtd_client_open_from_pipe(&pipe->client);
-+    pipe->path = path;
-+    pipe->fd = -1;
-+
-+    mode_t mode = S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
-+    if (mkfifo(path, mode)) {
-+        if (errno != EEXIST) {
-+            goto error;
-+        }
-+        mode &= ~lgtd_command_pipe_get_umask();
-+        if (chmod(path, mode)) {
-+            goto error;
-+        }
-+    }
-+
-+    pipe->fd = open(path, O_RDONLY|O_NONBLOCK);
-+    if (pipe->fd == -1) {
-+        goto error;
-+    }
-+
-+    if (evutil_make_socket_nonblocking(pipe->fd) == -1) {
-+        goto error;
-+    }
-+
-+    pipe->read_ev = event_new(
-+        lgtd_ev_base,
-+        pipe->fd,
-+        EV_READ|EV_PERSIST,
-+        lgtd_command_pipe_read_callback,
-+        pipe
-+    );
-+    if (!pipe->read_ev) {
-+        goto error;
-+    }
-+
-+    pipe->read_buf = evbuffer_new();
-+    if (!pipe->read_buf) {
-+        goto error;
-+    }
-+
-+    if (event_add(pipe->read_ev, NULL)) {
-+        goto error;
-+    }
-+
-+    lgtd_info("command pipe ready at %s", pipe->path);
-+
-+    SLIST_INSERT_HEAD(&lgtd_command_pipes, pipe, link);
-+
-+    return true;
-+
-+error:
-+    lgtd_warn("can't open command pipe %s", path);
-+    if (pipe->read_buf) {
-+        evbuffer_free(pipe->read_buf);
-+    }
-+    if (pipe->read_ev) {
-+        event_free(pipe->read_ev);
-+    }
-+    if (pipe->fd != -1) {
-+        close(pipe->fd);
-+    }
-+    free(pipe);
-+    return false;
-+}
-+
-+void
-+lgtd_command_pipe_close_all(void)
-+{
-+    while (!SLIST_EMPTY(&lgtd_command_pipes)) {
-+        lgtd_command_pipe_close(SLIST_FIRST(&lgtd_command_pipes));
-+    }
-+}
-diff --git a/core/pipe.h b/core/pipe.h
-new file mode 100644
---- /dev/null
-+++ b/core/pipe.h
-@@ -0,0 +1,33 @@
-+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
-+//
-+// This file is part of lighstd.
-+//
-+// lighstd is free software: you can redistribute it and/or modify
-+// it under the terms of the GNU General Public License as published by
-+// the Free Software Foundation, either version 3 of the License, or
-+// (at your option) any later version.
-+//
-+// lighstd is distributed in the hope that it will be useful,
-+// but WITHOUT ANY WARRANTY; without even the implied warranty of
-+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-+// GNU General Public License for more details.
-+//
-+// You should have received a copy of the GNU General Public License
-+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
-+
-+#pragma once
-+
-+struct lgtd_command_pipe {
-+    SLIST_ENTRY(lgtd_command_pipe)  link;
-+    const char                      *path;
-+    int                             fd;
-+    struct event                    *read_ev;
-+    struct evbuffer                 *read_buf;
-+    struct lgtd_client              client;
-+};
-+SLIST_HEAD(lgtd_command_pipe_list, lgtd_command_pipe);
-+
-+extern struct lgtd_command_pipe_list lgtd_command_pipes;
-+
-+bool lgtd_command_pipe_open(const char *);
-+void lgtd_command_pipe_close_all(void);
-diff --git a/core/proto.c b/core/proto.c
---- a/core/proto.c
-+++ b/core/proto.c
-@@ -178,7 +178,7 @@
-     )
- 
-     lgtd_client_start_send_response(client);
--    LGTD_CLIENT_WRITE_STRING(client, "[");
-+    lgtd_client_write_string(client, "[");
-     struct lgtd_router_device *device;
-     SLIST_FOREACH(device, devices, link) {
-         struct lgtd_lifx_bulb *bulb = device->device;
-@@ -204,22 +204,22 @@
-             );
-             continue;
-         }
--        LGTD_CLIENT_WRITE_STRING(client, buf);
-+        lgtd_client_write_string(client, buf);
- 
-         bool comma = false;
-         int tag_id;
-         LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) {
--            LGTD_CLIENT_WRITE_STRING(client, comma ? ",\"" : "\"");
--            LGTD_CLIENT_WRITE_STRING(client, bulb->gw->tags[tag_id]->label);
--            LGTD_CLIENT_WRITE_STRING(client, "\"");
-+            lgtd_client_write_string(client, comma ? ",\"" : "\"");
-+            lgtd_client_write_string(client, bulb->gw->tags[tag_id]->label);
-+            lgtd_client_write_string(client, "\"");
-             comma = true;
-         }
- 
--        LGTD_CLIENT_WRITE_STRING(
-+        lgtd_client_write_string(
-             client, SLIST_NEXT(device, link) ?  "]}," : "]}"
-         );
-     }
--    LGTD_CLIENT_WRITE_STRING(client, "]");
-+    lgtd_client_write_string(client, "]");
-     lgtd_client_end_send_response(client);
- 
-     lgtd_router_device_list_free(devices);
-diff --git a/tests/lightsc b/examples/lightsc.py
-rename from tests/lightsc
-rename to examples/lightsc.py
-diff --git a/examples/lightsc.sh b/examples/lightsc.sh
-new file mode 100644
---- /dev/null
-+++ b/examples/lightsc.sh
-@@ -0,0 +1,72 @@
-+#!/bin/sh
-+
-+# Here is an example script that dims bulbs to a warm orange:
-+
-+# #!/bin/sh
-+#
-+# # Optional (default value: /run/lightsd.cmd):
-+# COMMAND_PIPE=/foo/bar/lightsd.cmd
-+#
-+# . /usr/lib/lightsd/lightsc.sh
-+#
-+# lightsc set_light_from_hsbk ${*:-'"*"'} 37.469443 1.0 0.05 3500 600
-+
-+# Here is how you could use it:
-+#
-+# - dim all the bulbs: orange
-+# - dim the bulb named kitchen: orange '"kitchen"'
-+# - dim the bulb named kitchen and the bulbs tagged bedroom:
-+#   orange '["kitchen", "#bedroom"]'
-+#
-+# You can also load this file directly in your shell rc configuration file.
-+#
-+# NOTE: Keep in mind that arguments must be JSON, you will have to enclose
-+#       tags and labels into double quotes '"likethis"'. Also keep in mind
-+#       that the pipe is write-only you cannot read any result back.
-+
-+_b64e() {
-+    if type base64 >/dev/null 2>&1 ; then
-+        base64
-+    elif type b64encode >/dev/null 2>&1 ; then
-+        b64encode
-+    else
-+        cat >/dev/null
-+        echo null
-+    fi
-+}
-+
-+_gen_request_id() {
-+    if type dd >/dev/null 2>&1 ; then
-+        printf '"%s"' `dd if=/dev/urandom bs=8 count=1 2>&- | _b64e`
-+    else
-+        echo null
-+    fi
-+}
-+
-+lightsc() {
-+    if [ $# -lt 2 ] ; then
-+        echo >&2 "Usage: $0 METHOD PARAMS ..."
-+        return 1
-+    fi
-+
-+    local pipe=${COMMAND_PIPE:-/run/lightsd.cmd}
-+    if [ ! -p $pipe ] ; then
-+        echo >&2 "$pipe cannot be found, is lightsd running?"
-+        return 1
-+    fi
-+
-+    local method=$1 ; shift
-+    local params=$1 ; shift
-+    for target in $* ; do
-+        params=$params,$target
-+    done
-+
-+    tee $pipe <<EOF
-+{
-+  "jsonrpc": "2.0",
-+  "method": "$method",
-+  "params": [$params],
-+  "id": `_gen_request_id`
-+}
-+EOF
-+}
-diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt
---- a/tests/core/CMakeLists.txt
-+++ b/tests/core/CMakeLists.txt
-@@ -2,9 +2,11 @@
-     ${LIGHTSD_SOURCE_DIR}
-     ${LIGHTSD_SOURCE_DIR}/core/
-     ${CMAKE_CURRENT_SOURCE_DIR}
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../lifx
-     ${LIGHTSD_BINARY_DIR}
-     ${LIGHTSD_BINARY_DIR}/core/
-     ${CMAKE_CURRENT_BINARY_DIR}
-+    ${CMAKE_CURRENT_BINARY_DIR}/../lifx
- )
- 
- ADD_ALL_SUBDIRECTORIES()
-diff --git a/tests/core/jsonrpc/test_jsonrpc_utils.h b/tests/core/jsonrpc/test_jsonrpc_utils.h
---- a/tests/core/jsonrpc/test_jsonrpc_utils.h
-+++ b/tests/core/jsonrpc/test_jsonrpc_utils.h
-@@ -1,5 +1,7 @@
- #pragma once
- 
-+#include "mock_gateway.h"
-+
- #define TEST_REQUEST_INITIALIZER { NULL, NULL, 0, NULL }
- 
- static inline int
-diff --git a/tests/core/mock_client_buf.h b/tests/core/mock_client_buf.h
---- a/tests/core/mock_client_buf.h
-+++ b/tests/core/mock_client_buf.h
-@@ -21,3 +21,15 @@
-     client_write_buf_idx += to_write;
-     return 0;
- }
-+
-+void
-+lgtd_client_write_string(struct lgtd_client *client, const char *msg)
-+{
-+    bufferevent_write(client->io, msg, strlen(msg));
-+}
-+
-+void
-+lgtd_client_write_buf(struct lgtd_client *client, const char *buf, int bufsz)
-+{
-+    bufferevent_write(client->io, buf, bufsz);
-+}
-diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h
-new file mode 100644
---- /dev/null
-+++ b/tests/core/mock_event2.h
-@@ -0,0 +1,109 @@
-+#pragma once
-+
-+#ifndef MOCKED_EVBUFFER_DRAIN
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    (void)buf;
-+    (void)len;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_NEW
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_FREE
-+void
-+evbuffer_free(struct evbuffer *buf)
-+{
-+    (void)buf;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_GET_LENGTH
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    (void)buf;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_PULLUP
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    (void)buf;
-+    (void)size;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVBUFFER_READ
-+int
-+evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch)
-+{
-+    (void)buffer;
-+    (void)fd;
-+    return howmuch;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_ADD
-+int
-+event_add(struct event *ev, const struct timeval *timeout)
-+{
-+    (void)ev;
-+    (void)timeout;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_DEL
-+int
-+event_del(struct event *ev)
-+{
-+    (void)ev;
-+    return 0;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_FREE
-+void
-+event_free(struct event *ev)
-+{
-+    (void)ev;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVENT_NEW
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
-+int
-+evutil_make_socket_nonblocking(evutil_socket_t fd)
-+{
-+    (void)fd;
-+    return 0;
-+}
-+#endif
-diff --git a/tests/core/pipe/CMakeLists.txt b/tests/core/pipe/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/CMakeLists.txt
-@@ -0,0 +1,26 @@
-+INCLUDE_DIRECTORIES(
-+    ${CMAKE_CURRENT_SOURCE_DIR}
-+    ${CMAKE_CURRENT_BINARY_DIR}
-+)
-+
-+ADD_LIBRARY(
-+    test_core_pipe STATIC
-+    ${LIGHTSD_SOURCE_DIR}/core/jsmn.c
-+    ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
-+)
-+
-+TARGET_LINK_LIBRARIES(test_core_pipe ${TIME_MONOTONIC_LIBRARY})
-+
-+FUNCTION(ADD_PIPE_TEST TEST_SOURCE)
-+    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_pipe)
-+ENDFUNCTION()
-+
-+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
-+FOREACH(TEST ${TESTS})
-+    ADD_PIPE_TEST(${TEST})
-+ENDFOREACH()
-diff --git a/tests/core/pipe/test_pipe_close.c b/tests/core/pipe/test_pipe_close.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_close.c
-@@ -0,0 +1,116 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVENT_DEL
-+#define MOCKED_EVBUFFER_FREE
-+#define MOCKED_EVENT_FREE
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+
-+#include "tests_utils.h"
-+#include "tests_pipe_utils.h"
-+
-+char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int event_del_call_count = 0;
-+
-+int
-+event_del(struct event *ev)
-+{
-+    (void)ev;
-+
-+    event_del_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_free_call_count = 0;
-+
-+void
-+evbuffer_free(struct evbuffer *buf)
-+{
-+    (void)buf;
-+
-+    evbuffer_free_call_count++;
-+}
-+
-+static int event_free_call_count = 0;
-+
-+void
-+event_free(struct event *ev)
-+{
-+    (void)ev;
-+
-+    event_free_call_count++;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    int pipe_fd = SLIST_FIRST(&lgtd_command_pipes)->fd;
-+
-+    lgtd_command_pipe_close_all();
-+
-+    if (event_del_call_count != 1) {
-+        errx(1, "event_del_call_count = %d", event_del_call_count);
-+    }
-+    if (evbuffer_free_call_count != 1) {
-+        errx(1, "evbuffer_free_call_count = %d", evbuffer_free_call_count);
-+    }
-+    if (event_free_call_count != 1) {
-+        errx(1, "event_free_call_count = %d", event_free_call_count);
-+    }
-+    struct stat sb;
-+    if (fstat(pipe_fd, &sb) != -1 && errno != EBADF) {
-+        errx(1, "the pipe file descriptor wasn't closed correctly");
-+    }
-+    if (stat(path, &sb) != -1 && errno != ENOENT) {
-+        errx(1, "the pipe wasn't removed correctly");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_open.c b/tests/core/pipe/test_pipe_open.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_open.c
-@@ -0,0 +1,170 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVENT_ADD
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_CLIENT_OPEN_FROM_PIPE
-+#include "tests_pipe_utils.h"
-+
-+char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static bool make_socket_nonblocking_call_count = 0;
-+
-+int 
-+evutil_make_socket_nonblocking(evutil_socket_t fd)
-+{
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in make_socket_nonblocking", fd);
-+    }
-+
-+    make_socket_nonblocking_call_count++;
-+
-+    return 0;
-+}
-+
-+static int event_new_call_count = 0;
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    if (base != lgtd_ev_base) {
-+        errx(
-+            1, "unexpected lgtd_ev_base = %p (expected %p)",
-+            base, lgtd_ev_base
-+        );
-+    }
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in event_new", fd);
-+    }
-+    if (events != (EV_READ|EV_PERSIST)) {
-+        errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST);
-+    }
-+    if (cb != lgtd_command_pipe_read_callback) {
-+        errx(1, "the read callback wasn't set correctly");
-+    }
-+    if (!ctx) {
-+        errx(1, "the callback context wasn't set correctly");
-+    }
-+
-+    event_new_call_count++;
-+
-+    return (void *)1;
-+}
-+
-+static int evbuffer_new_call_count = 0;
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    evbuffer_new_call_count++;
-+
-+    return (void *)2;
-+}
-+
-+static int event_add_call_count = 0;
-+
-+int
-+event_add(struct event *ev, const struct timeval *timeout)
-+{
-+    if (ev != (void *)1) {
-+        errx(1, "got unexpected event %p (expected %p)", ev, (void*)1);
-+    }
-+
-+    if (timeout) {
-+        errx(1, "a timeout shouldn't have been passed");
-+    }
-+
-+    event_add_call_count++;
-+
-+    return 0;
-+}
-+
-+static int client_open_from_pipe_call_count = 0;
-+
-+void
-+lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-+{
-+    if (!pipe_client) {
-+        errx(1, "missing pipe_client");
-+    }
-+
-+    client_open_from_pipe_call_count++;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    if (make_socket_nonblocking_call_count != 1) {
-+        errx(
-+            1, "make_socket_nonblocking_call_count = %d",
-+            make_socket_nonblocking_call_count
-+        );
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+    if (evbuffer_new_call_count != 1) {
-+        errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count);
-+    }
-+    if (event_add_call_count != 1) {
-+        errx(1, "event_add_call_count = %d", event_add_call_count);
-+    }
-+    if (SLIST_EMPTY(&lgtd_command_pipes)) {
-+        errx(1, "the list of command pipes shouldn't be empty");
-+    }
-+
-+    struct stat sb;
-+    if (stat(path, &sb)) {
-+        errx(1, "can't stat pipe %s", path);
-+    }
-+
-+    mode_t expected_mode;
-+    expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
-+    expected_mode &= ~lgtd_command_pipe_get_umask();
-+    if (sb.st_mode != expected_mode) {
-+        errx(
-+            1, "unexpected mode %o (expected %o)",
-+            sb.st_mode, expected_mode
-+        );
-+    }
-+
-+    // make sure it's idempotent:
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_open_fifo_already_exists.c b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
-@@ -0,0 +1,175 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVUTIL_MAKE_SOCKET_NONBLOCKING
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVENT_ADD
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_CLIENT_OPEN_FROM_PIPE
-+#include "tests_pipe_utils.h"
-+
-+char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static bool make_socket_nonblocking_call_count = 0;
-+
-+int 
-+evutil_make_socket_nonblocking(evutil_socket_t fd)
-+{
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in make_socket_nonblocking", fd);
-+    }
-+
-+    make_socket_nonblocking_call_count++;
-+
-+    return 0;
-+}
-+
-+static int event_new_call_count = 0;
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    if (base != lgtd_ev_base) {
-+        errx(
-+            1, "unexpected lgtd_ev_base = %p (expected %p)",
-+            base, lgtd_ev_base
-+        );
-+    }
-+    if (fd <= 0) {
-+        errx(1, "got invalid fd %d in event_new", fd);
-+    }
-+    if (events != (EV_READ|EV_PERSIST)) {
-+        errx(1, "got events %#x (expected %#x)", events, EV_READ|EV_PERSIST);
-+    }
-+    if (cb != lgtd_command_pipe_read_callback) {
-+        errx(1, "the read callback wasn't set correctly");
-+    }
-+    if (!ctx) {
-+        errx(1, "the callback context wasn't set correctly");
-+    }
-+
-+    event_new_call_count++;
-+
-+    return (void *)1;
-+}
-+
-+static int evbuffer_new_call_count = 0;
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    evbuffer_new_call_count++;
-+
-+    return (void *)2;
-+}
-+
-+static int event_add_call_count = 0;
-+
-+int
-+event_add(struct event *ev, const struct timeval *timeout)
-+{
-+    if (ev != (void *)1) {
-+        errx(1, "got unexpected event %p (expected %p)", ev, (void*)1);
-+    }
-+
-+    if (timeout) {
-+        errx(1, "a timeout shouldn't have been passed");
-+    }
-+
-+    event_add_call_count++;
-+
-+    return 0;
-+}
-+
-+static int client_open_from_pipe_call_count = 0;
-+
-+void
-+lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-+{
-+    if (!pipe_client) {
-+        errx(1, "missing pipe_client");
-+    }
-+
-+    client_open_from_pipe_call_count++;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+
-+    if (mkfifo(path, 0)) {
-+        errx(1, "can't open fifo");
-+    }
-+
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    if (make_socket_nonblocking_call_count != 1) {
-+        errx(
-+            1, "make_socket_nonblocking_call_count = %d",
-+            make_socket_nonblocking_call_count
-+        );
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+    if (evbuffer_new_call_count != 1) {
-+        errx(1, "evbuffer_new_call_count = %d", evbuffer_new_call_count);
-+    }
-+    if (event_add_call_count != 1) {
-+        errx(1, "event_add_call_count = %d", event_add_call_count);
-+    }
-+    if (SLIST_EMPTY(&lgtd_command_pipes)) {
-+        errx(1, "the list of command pipes shouldn't be empty");
-+    }
-+
-+    struct stat sb;
-+    if (stat(path, &sb)) {
-+        errx(1, "can't stat pipe %s", path);
-+    }
-+
-+    mode_t expected_mode;
-+    expected_mode = S_IFIFO|S_IWUSR|S_IRUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IWGRP;
-+    expected_mode &= ~lgtd_command_pipe_get_umask();
-+    if (sb.st_mode != expected_mode) {
-+        errx(
-+            1, "unexpected mode %o (expected %o)",
-+            sb.st_mode, expected_mode
-+        );
-+    }
-+
-+    // make sure it's idempotent:
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+    if (event_new_call_count != 1) {
-+        errx(1, "event_new_call_count = %d", event_new_call_count);
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_read_callback.c b/tests/core/pipe/test_pipe_read_callback.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_read_callback.c
-@@ -0,0 +1,191 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVBUFFER_READ
-+#define MOCKED_EVBUFFER_PULLUP
-+#define MOCKED_EVBUFFER_GET_LENGTH
-+#define MOCKED_EVBUFFER_DRAIN
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_JSONRPC_DISPATCH_REQUEST
-+#include "tests_pipe_utils.h"
-+
-+static unsigned char request[] = ("{"
-+    "\"jsonrpc\": \"2.0\","
-+    "\"method\": \"get_light_state\","
-+    "\"params\": [\"*\"],"
-+    "\"id\": 42"
-+"}");
-+
-+static char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static int jsonrpc_dispatch_request_call_count = 0;
-+
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+
-+    if (!parsed) {
-+        errx(1, "number of parsed json tokens not passed in");
-+    }
-+
-+    if (memcmp(client->json, request, sizeof(request))) {
-+        errx(1, "got unexpected json");
-+    }
-+
-+    jsonrpc_dispatch_request_call_count++;
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+static int
-+get_nbytes_read(int call_count)
-+{
-+    switch (call_count) {
-+    case 0:
-+        return sizeof(request) - 1; // we don't return the '\0'
-+    default:
-+        return 0;
-+    }
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int evbuffer_drain_call_count = 0;
-+
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    jsmn_parser jsmn_ctx;
-+    jsmn_init(&jsmn_ctx);
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
-+        errx(1, "the client json parser context wasn't re-initialized");
-+    }
-+
-+
-+    switch (evbuffer_drain_call_count) {
-+    case 0:
-+        if (len != sizeof(request) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(request) - 1
-+            );
-+        }
-+        break;
-+    default:
-+        break;
-+    }
-+    evbuffer_drain_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_pullup_call_count = 0;
-+
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    if (size != -1) {
-+        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
-+    }
-+
-+    return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0];
-+}
-+
-+static int evbuffer_get_length_call_count = 0;
-+
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    return get_nbytes_read(evbuffer_get_length_call_count++);
-+}
-+
-+static int evbuffer_read_call_count = 0;
-+
-+int
-+evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (fd != pipe->fd) {
-+        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
-+    }
-+
-+    if (howmuch != -1) {
-+        errx(
-+            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
-+        );
-+    }
-+
-+    return get_nbytes_read(evbuffer_read_call_count++);
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+
-+    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_read_callback_extra_data.c b/tests/core/pipe/test_pipe_read_callback_extra_data.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_read_callback_extra_data.c
-@@ -0,0 +1,218 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVBUFFER_READ
-+#define MOCKED_EVBUFFER_PULLUP
-+#define MOCKED_EVBUFFER_GET_LENGTH
-+#define MOCKED_EVBUFFER_DRAIN
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_JSONRPC_DISPATCH_REQUEST
-+#include "tests_pipe_utils.h"
-+
-+#define REQUEST_1 "{"                   \
-+    "\"jsonrpc\": \"2.0\","             \
-+    "\"method\": \"get_light_state\","  \
-+    "\"params\": [\"*\"],"              \
-+    "\"id\": 42"                        \
-+"}"
-+#define EXTRA_DATA "BLUBLBULBUBUHIFESHFUSsoundsaboutright" 
-+
-+static unsigned char request[] = (
-+    REQUEST_1
-+    EXTRA_DATA
-+);
-+
-+static char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static int jsonrpc_dispatch_request_call_count = 0;
-+
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+
-+    if (!parsed) {
-+        errx(1, "number of parsed json tokens not passed in");
-+    }
-+
-+    if (memcmp(client->json, request, sizeof(request))) {
-+        errx(1, "got unexpected json");
-+    }
-+
-+    jsonrpc_dispatch_request_call_count++;
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+static int
-+get_nbytes_read(int call_count)
-+{
-+    switch (call_count) {
-+    case 0:
-+        return sizeof(request) - 1; // we don't return the '\0'
-+    default:
-+        return 0;
-+    }
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int evbuffer_drain_call_count = 0;
-+
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    jsmn_parser jsmn_ctx;
-+    jsmn_init(&jsmn_ctx);
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
-+        errx(1, "the client json parser context wasn't re-initialized");
-+    }
-+
-+    switch (evbuffer_drain_call_count) {
-+    case 0:
-+        if (len != sizeof(REQUEST_1) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(request) - 1
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (len != sizeof(request) - sizeof(REQUEST_1)) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, sizeof(request) - sizeof(REQUEST_1)
-+            );
-+        }
-+        break;
-+    default:
-+        break;
-+    }
-+    evbuffer_drain_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_pullup_call_count = 0;
-+
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    if (size != -1) {
-+        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
-+    }
-+
-+    return &request[evbuffer_pullup_call_count++ ? sizeof(request) - 1 : 0];
-+}
-+
-+static int evbuffer_get_length_call_count = 0;
-+
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    size_t len;
-+    switch (evbuffer_get_length_call_count) {
-+    case 0:
-+        len = sizeof(request) - 1;
-+        break;
-+    case 1:
-+        len = sizeof(request) - sizeof(REQUEST_1);
-+        break;
-+    default:
-+        len = 0;
-+        break;
-+    }
-+    evbuffer_get_length_call_count++;
-+
-+    return len;
-+}
-+
-+static int evbuffer_read_call_count = 0;
-+
-+int
-+evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (fd != pipe->fd) {
-+        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
-+    }
-+
-+    if (howmuch != -1) {
-+        errx(
-+            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
-+        );
-+    }
-+
-+    return get_nbytes_read(evbuffer_read_call_count++);
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+
-+    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/test_pipe_read_callback_multiple_requests.c b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/test_pipe_read_callback_multiple_requests.c
-@@ -0,0 +1,258 @@
-+#include "pipe.c"
-+
-+#include <sys/tree.h>
-+#include <endian.h>
-+#include <limits.h>
-+
-+#include "lifx/wire_proto.h"
-+
-+#define MOCKED_EVENT_NEW
-+#define MOCKED_EVBUFFER_NEW
-+#define MOCKED_EVBUFFER_READ
-+#define MOCKED_EVBUFFER_PULLUP
-+#define MOCKED_EVBUFFER_GET_LENGTH
-+#define MOCKED_EVBUFFER_DRAIN
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+
-+#include "tests_utils.h"
-+#define MOCKED_JSONRPC_DISPATCH_REQUEST
-+#include "tests_pipe_utils.h"
-+
-+#define REQUEST_1 "{"                   \
-+    "\"jsonrpc\": \"2.0\","             \
-+    "\"method\": \"get_light_state\","  \
-+    "\"params\": [\"*\"],"              \
-+    "\"id\": 42"                        \
-+"}"
-+
-+#define REQUEST_2 "{"           \
-+    "\"jsonrpc\": \"2.0\","     \
-+    "\"method\": \"power_on\"," \
-+    "\"params\": [\"*\"],"      \
-+    "\"id\": 43"                \
-+"}"
-+
-+static unsigned char request[] = (
-+    REQUEST_1
-+    REQUEST_2
-+);
-+
-+static char *tmpdir = NULL;
-+
-+void
-+cleanup_tmpdir(void)
-+{
-+    lgtd_tests_remove_temp_dir(tmpdir);
-+}
-+
-+static int jsonrpc_dispatch_request_call_count = 0;
-+
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+
-+    if (!parsed) {
-+        errx(1, "number of parsed json tokens not passed in");
-+    }
-+
-+    switch (jsonrpc_dispatch_request_call_count) {
-+    case 0:
-+        if (memcmp(client->json, request, sizeof(request) - 1)) {
-+            errx(
-+                1, "got unexpected json %s (expected %s)",
-+                client->json, request
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (memcmp(client->json, REQUEST_2, sizeof(REQUEST_2) - 1)) {
-+            errx(
-+                1, "got unexpected json %s (expected %s)",
-+                client->json, REQUEST_2
-+            );
-+        }
-+        break;
-+    default:
-+        errx(
-+            1, "jsonrpc_dispatch_request_call_count = %d",
-+            jsonrpc_dispatch_request_call_count
-+        );
-+        break;
-+    }
-+
-+    jsonrpc_dispatch_request_call_count++;
-+}
-+
-+struct event *
-+event_new(struct event_base *base,
-+          evutil_socket_t fd,
-+          short events,
-+          event_callback_fn cb,
-+          void *ctx)
-+{
-+    (void)base;
-+    (void)fd;
-+    (void)events;
-+    (void)cb;
-+    (void)ctx;
-+
-+    return (void *)1;
-+}
-+
-+struct evbuffer *
-+evbuffer_new(void)
-+{
-+    return (void *)2;
-+}
-+
-+static int evbuffer_drain_call_count = 0;
-+
-+int
-+evbuffer_drain(struct evbuffer *buf, size_t len)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    jsmn_parser jsmn_ctx;
-+    jsmn_init(&jsmn_ctx);
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (memcmp(&pipe->client.jsmn_ctx, &jsmn_ctx, sizeof(jsmn_ctx))) {
-+        errx(1, "the client json parser context wasn't re-initialized");
-+    }
-+
-+    switch (evbuffer_drain_call_count) {
-+    case 0:
-+        if (len != sizeof(REQUEST_1) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(REQUEST_1) - 1
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (len != sizeof(REQUEST_2) - 1) {
-+            errx(
-+                1, "trying to drain %ju bytes (expected %ju)",
-+                (uintmax_t)len, (uintmax_t)sizeof(REQUEST_2) - 1
-+            );
-+        }
-+        break;
-+    default:
-+        errx(1, "evbuffer_drain_call_count = %d", evbuffer_drain_call_count);
-+        break;
-+    }
-+    evbuffer_drain_call_count++;
-+
-+    return 0;
-+}
-+
-+static int evbuffer_pullup_call_count = 0;
-+
-+unsigned char *
-+evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    if (size != -1) {
-+        errx(1, "got unexpected size %ld in pullup (expected -1)", size);
-+    }
-+
-+    int offset;
-+    switch (evbuffer_pullup_call_count) {
-+    case 0:
-+        offset = 0;
-+        break;
-+    case 1:
-+        offset = sizeof(REQUEST_1) - 1;
-+        break;
-+    default:
-+        offset = sizeof(request);
-+        break;
-+    }
-+    evbuffer_pullup_call_count++;
-+
-+    return &request[offset];
-+}
-+
-+static int evbuffer_get_length_call_count = 0;
-+
-+size_t
-+evbuffer_get_length(const struct evbuffer *buf)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    size_t len;
-+    switch (evbuffer_get_length_call_count) {
-+    case 0:
-+        len = sizeof(request) - 1;
-+        break;
-+    case 1:
-+        len = sizeof(request) - sizeof(REQUEST_1);
-+        break;
-+    default:
-+        len = 0;
-+        break;
-+    }
-+    evbuffer_get_length_call_count++;
-+
-+    return len;
-+}
-+
-+static int evbuffer_read_call_count = 0;
-+
-+int
-+evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
-+{
-+    if (buf != (void *)2) {
-+        errx(1, "got unexpected buf %p (expected %p)", buf, (void *)2);
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+    if (fd != pipe->fd) {
-+        errx(1, "got unexpected fd %d (expected %d)", fd, pipe->fd);
-+    }
-+
-+    if (howmuch != -1) {
-+        errx(
-+            1, "got unexpected howmuch bytes to read %d (expected -1)", howmuch
-+        );
-+    }
-+
-+    int rv = 0;
-+    switch (evbuffer_read_call_count) {
-+    case 0:
-+        rv = sizeof(request) - 1;
-+    default:
-+        break;
-+    }
-+    evbuffer_read_call_count++;
-+
-+    return rv;
-+}
-+
-+int
-+main(void)
-+{
-+    tmpdir = lgtd_tests_make_temp_dir();
-+    atexit(cleanup_tmpdir);
-+
-+    char path[PATH_MAX] = { 0 };
-+    snprintf(path, sizeof(path), "%s/lightsd.pipe", tmpdir);
-+    if (!lgtd_command_pipe_open(path)) {
-+        errx(1, "couldn't open pipe");
-+    }
-+
-+    struct lgtd_command_pipe *pipe = SLIST_FIRST(&lgtd_command_pipes);
-+
-+    lgtd_command_pipe_read_callback(pipe->fd, EV_READ, pipe);
-+
-+    return 0;
-+}
-diff --git a/tests/core/pipe/tests_pipe_utils.h b/tests/core/pipe/tests_pipe_utils.h
-new file mode 100644
---- /dev/null
-+++ b/tests/core/pipe/tests_pipe_utils.h
-@@ -0,0 +1,19 @@
-+#pragma once
-+
-+#ifndef MOCKED_CLIENT_OPEN_FROM_PIPE
-+void
-+lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-+{
-+    memset(pipe_client, 0, sizeof(*pipe_client));
-+    jsmn_init(&pipe_client->jsmn_ctx);
-+}
-+#endif
-+
-+#ifndef MOCKED_JSONRPC_DISPATCH_REQUEST
-+void
-+lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
-+{
-+    (void)client;
-+    (void)parsed;
-+}
-+#endif
-diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt
---- a/tests/core/proto/CMakeLists.txt
-+++ b/tests/core/proto/CMakeLists.txt
-@@ -8,6 +8,7 @@
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-     ${LIGHTSD_SOURCE_DIR}/core/jsonrpc.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/timer.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-@@ -15,11 +16,11 @@
- )
- TARGET_LINK_LIBRARIES(test_core_proto ${TIME_MONOTONIC_LIBRARY})
- 
--FUNCTION(ADD_ROUTER_TEST TEST_SOURCE)
-+FUNCTION(ADD_PROTO_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto)
- ENDFUNCTION()
- 
- FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
- FOREACH(TEST ${TESTS})
--    ADD_ROUTER_TEST(${TEST})
-+    ADD_PROTO_TEST(${TEST})
- ENDFOREACH()
-diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c
---- a/tests/core/proto/test_proto_get_light_state.c
-+++ b/tests/core/proto/test_proto_get_light_state.c
-@@ -29,6 +29,9 @@
-     static struct lgtd_router_device_list devices =
-         SLIST_HEAD_INITIALIZER(&devices);
- 
-+    static struct lgtd_lifx_gateway gw_bulb_1 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs)
-+    };
-     static struct lgtd_lifx_bulb bulb_1 = {
-         .addr = { 1, 2, 3, 4, 5 },
-         .state = {
-@@ -39,7 +42,8 @@
-             .label = "wave",
-             .power = LGTD_LIFX_POWER_ON,
-             .tags = 0
--        }
-+        },
-+        .gw = &gw_bulb_1
-     };
-     static struct lgtd_router_device device_1 = { .device = &bulb_1 };
-     SLIST_INSERT_HEAD(&devices, &device_1, link);
-@@ -76,7 +80,7 @@
- int
- main(void)
- {
--    struct lgtd_client client;
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-     struct lgtd_proto_target_list *targets = (void *)0x2a;
- 
-     lgtd_proto_get_light_state(&client, targets);
-@@ -103,7 +107,7 @@
-             1,
-             "%d bytes written, expected %lu "
-             "(got %.*s instead of %s)",
--            client_write_buf_idx, sizeof(expected) - 1,
-+            client_write_buf_idx, sizeof(expected) - 1UL,
-             client_write_buf_idx, client_write_buf, expected
-         );
-     }
-diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c
---- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c
-+++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c
-@@ -35,7 +35,7 @@
- int
- main(void)
- {
--    struct lgtd_client client;
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-     struct lgtd_proto_target_list *targets = (void *)0x2a;
- 
-     lgtd_proto_get_light_state(&client, targets);
-@@ -45,7 +45,7 @@
-     if (client_write_buf_idx != sizeof(expected) - 1) {
-         lgtd_errx(
-             1, "%d bytes written, expected %lu",
--            client_write_buf_idx, sizeof(expected) - 1
-+            client_write_buf_idx, sizeof(expected) - 1UL
-         );
-     }
- 
-diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c
---- a/tests/core/proto/test_proto_get_light_state_null_device_list.c
-+++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c
-@@ -45,7 +45,7 @@
- int
- main(void)
- {
--    struct lgtd_client client;
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-     struct lgtd_proto_target_list *targets = (void *)0x2a;
- 
-     lgtd_proto_get_light_state(&client, targets);
-diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c
---- a/tests/core/proto/test_proto_power_off.c
-+++ b/tests/core/proto/test_proto_power_off.c
-@@ -52,7 +52,7 @@
-     struct lgtd_proto_target_list *targets;
-     targets = lgtd_tests_build_target_list("*", NULL);
- 
--    struct lgtd_client client;
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
- 
-     lgtd_proto_power_off(&client, targets);
- 
-diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c
---- a/tests/core/proto/test_proto_power_off_routing_error.c
-+++ b/tests/core/proto/test_proto_power_off_routing_error.c
-@@ -52,7 +52,7 @@
-     struct lgtd_proto_target_list *targets;
-     targets = lgtd_tests_build_target_list("*", NULL);
- 
--    struct lgtd_client client;
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
- 
-     lgtd_proto_power_off(&client, targets);
- 
-diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c
---- a/tests/core/proto/test_proto_power_on.c
-+++ b/tests/core/proto/test_proto_power_on.c
-@@ -52,7 +52,7 @@
-     struct lgtd_proto_target_list *targets;
-     targets = lgtd_tests_build_target_list("*", NULL);
- 
--    struct lgtd_client client;
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
- 
-     lgtd_proto_power_on(&client, targets);
- 
-diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c
---- a/tests/core/proto/test_proto_power_on_routing_error.c
-+++ b/tests/core/proto/test_proto_power_on_routing_error.c
-@@ -52,7 +52,7 @@
-     struct lgtd_proto_target_list *targets;
-     targets = lgtd_tests_build_target_list("*", NULL);
- 
--    struct lgtd_client client;
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
- 
-     lgtd_proto_power_on(&client, targets);
- 
-diff --git a/tests/core/proto/tests_proto_utils.h b/tests/core/proto/tests_proto_utils.h
---- a/tests/core/proto/tests_proto_utils.h
-+++ b/tests/core/proto/tests_proto_utils.h
-@@ -1,5 +1,9 @@
- #pragma once
- 
-+#include "mock_gateway.h"
-+
-+#define FAKE_BUFFEREVENT (void *)0xfeed
-+
- void
- lgtd_client_start_send_response(struct lgtd_client *client)
- {
-@@ -16,7 +20,7 @@
- void
- lgtd_client_send_response(struct lgtd_client *client, const char *msg)
- {
--    LGTD_CLIENT_WRITE_STRING(client, msg);
-+    lgtd_client_write_string(client, msg);
- }
- #endif
- 
-diff --git a/tests/core/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt
---- a/tests/core/router/CMakeLists.txt
-+++ b/tests/core/router/CMakeLists.txt
-@@ -8,6 +8,7 @@
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-     ${LIGHTSD_SOURCE_DIR}/core/proto.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/timer.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-diff --git a/tests/core/router/tests_router_utils.h b/tests/core/router/tests_router_utils.h
---- a/tests/core/router/tests_router_utils.h
-+++ b/tests/core/router/tests_router_utils.h
-@@ -1,5 +1,7 @@
- #pragma once
- 
-+#include "mock_gateway.h"
-+
- int lgtd_tests_gw_pkt_queue_size = 0;
- struct {
-     struct lgtd_lifx_gateway        *gw;
-diff --git a/tests/core/tests_shims.c b/tests/core/tests_shims.c
---- a/tests/core/tests_shims.c
-+++ b/tests/core/tests_shims.c
-@@ -30,52 +30,3 @@
- lgtd_cleanup(void)
- {
- }
--
--
--void lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw,
--                                          const struct lgtd_lifx_packet_header *hdr,
--                                          const struct lgtd_lifx_packet_pan_gateway *pkt)
--{
--    (void)gw;
--    (void)hdr;
--    (void)pkt;
--}
--
--void lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw,
--                                           const struct lgtd_lifx_packet_header *hdr,
--                                           const struct lgtd_lifx_packet_light_status *pkt)
--{
--    (void)gw;
--    (void)hdr;
--    (void)pkt;
--}
--
--void lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw,
--                                          const struct lgtd_lifx_packet_header *hdr,
--                                          const struct lgtd_lifx_packet_power_state *pkt)
--{
--    (void)gw;
--    (void)hdr;
--    (void)pkt;
--}
--
--void lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw,
--                                         const struct lgtd_lifx_packet_header *hdr,
--                                         const struct lgtd_lifx_packet_tag_labels *pkt)
--{
--    (void)gw;
--    (void)hdr;
--    (void)pkt;
--}
--
--struct lgtd_lifx_tag *
--lgtd_lifx_tagging_find_tag(const char *tag_label)
--{
--    struct lgtd_lifx_tag *tag = NULL;
--    LIST_FOREACH(tag, &lgtd_lifx_tags, link) {
--        if (!strcmp(tag->label, tag_label)) {
--            break;
--        }
--    }
--    return tag;
--}
-diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
---- a/tests/core/tests_utils.c
-+++ b/tests/core/tests_utils.c
-@@ -2,13 +2,18 @@
- #include <sys/tree.h>
- #include <sys/socket.h>
- #include <assert.h>
-+#include <dirent.h>
- #include <endian.h>
-+#include <err.h>
-+#include <limits.h>
- #include <netinet/in.h>
- #include <stdarg.h>
- #include <stdbool.h>
- #include <stdlib.h>
- #include <stdint.h>
-+#include <stdio.h>
- #include <string.h>
-+#include <unistd.h>
- 
- #include <event2/util.h>
- 
-@@ -26,9 +31,6 @@
- struct lgtd_lifx_gateway_list lgtd_lifx_gateways =
-     LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways);
- 
--struct lgtd_lifx_tag_list lgtd_lifx_tags =
--    LIST_HEAD_INITIALIZER(&lgtd_lifx_tags);
--
- struct lgtd_lifx_gateway *
- lgtd_tests_insert_mock_gateway(int id)
- {
-@@ -112,3 +114,42 @@
-     LIST_INSERT_HEAD(&tag->sites, site, link);
-     return site;
- }
-+
-+char *
-+lgtd_tests_make_temp_dir(void)
-+{
-+    char buf[PATH_MAX] = { 0 };
-+    int n = snprintf(buf, sizeof(buf), "%s/lightsd.test.XXXXXXXX", P_tmpdir);
-+    if (n >= (int)sizeof(buf)) {
-+        errx(1, "cannot allocate temporary directory");
-+    }
-+    return strdup(mkdtemp(buf));
-+}
-+
-+void
-+lgtd_tests_remove_temp_dir(char *path)
-+{
-+    DIR *tmpdir = opendir(path);
-+    if (!tmpdir) {
-+        return;
-+    }
-+
-+    struct dirent db;
-+    struct dirent *entry = &db;
-+    while (!readdir_r(tmpdir, entry, &entry) && entry) {
-+        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
-+            continue;
-+        }
-+        char buf[PATH_MAX] = { 0 };
-+        snprintf(buf, sizeof(buf), "%s/%s", path, entry->d_name);
-+        unlink(buf);
-+    }
-+
-+    closedir(tmpdir);
-+
-+    if (rmdir(path)) {
-+        warn("couldn't remove tempdir %s", path);
-+    }
-+
-+    free(path);
-+}
-diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h
---- a/tests/core/tests_utils.h
-+++ b/tests/core/tests_utils.h
-@@ -30,6 +30,9 @@
-     return true;
- }
- 
-+char *lgtd_tests_make_temp_dir(void);
-+void lgtd_tests_remove_temp_dir(char *);
-+
- struct lgtd_lifx_gateway *lgtd_tests_insert_mock_gateway(int);
- struct lgtd_lifx_bulb *lgtd_tests_insert_mock_bulb(struct lgtd_lifx_gateway *, uint64_t);
- struct lgtd_proto_target_list *lgtd_tests_build_target_list(const char *, ...);
-diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/mock_gateway.h
-@@ -0,0 +1,119 @@
-+#pragma once
-+
-+#include "core/time_monotonic.h"
-+#include "lifx/bulb.h"
-+#include "lifx/gateway.h"
-+
-+struct lgtd_lifx_tag;
-+struct lgtd_lifx_gateway;
-+
-+#ifndef MOCKED_LIFX_GATEWAY_SEND_TO_SITE
-+bool
-+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-+                               enum lgtd_lifx_packet_type pkt_type,
-+                               const void *pkt)
-+{
-+    (void)gw;
-+    (void)pkt_type;
-+    (void)pkt;
-+    return false;
-+}
-+#endif
-+
-+#ifndef MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID
-+int
-+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw,
-+                                  int tag_id,
-+                                  const char *tag_label)
-+{
-+    (void)gw;
-+    (void)tag_id;
-+    (void)tag_label;
-+    return -1;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PAN_GATEWAY
-+void
-+lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw,
-+                                     const struct lgtd_lifx_packet_header *hdr,
-+                                     const struct lgtd_lifx_packet_pan_gateway *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_LIGHT_STATUS
-+void
-+lgtd_lifx_gateway_handle_light_status(struct lgtd_lifx_gateway *gw,
-+                                      const struct lgtd_lifx_packet_header *hdr,
-+                                      const struct lgtd_lifx_packet_light_status *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_POWER_STATE
-+void
-+lgtd_lifx_gateway_handle_power_state(struct lgtd_lifx_gateway *gw,
-+                                     const struct lgtd_lifx_packet_header *hdr,
-+                                     const struct lgtd_lifx_packet_power_state *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAG_LABELS
-+void
-+lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *gw,
-+                                    const struct lgtd_lifx_packet_header *hdr,
-+                                    const struct lgtd_lifx_packet_tag_labels *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_DEALLOCATE_TAG_ID
-+void
-+lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id)
-+{
-+    (void)gw;
-+    (void)tag_id;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_GET_TAG_ID
-+int
-+lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw,
-+                             const struct lgtd_lifx_tag *tag)
-+{
-+    int tag_id;
-+    LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) {
-+        if (gw->tags[tag_id] == tag) {
-+            return tag_id;
-+        }
-+    }
-+
-+    return -1;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS
-+void
-+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw,
-+                                       uint64_t bulb_tags,
-+                                       uint64_t pkt_tags)
-+{
-+    (void)gw;
-+    (void)bulb_tags;
-+    (void)pkt_tags;
-+}
-+#endif
--- a/add_daemon_module.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1323 +0,0 @@
-# HG changeset patch
-# Parent  fa84fd51301cf3bf3786f7e3c03031488f31d1ef
-Actual support for daemonization with a nice process name
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -1,4 +1,4 @@
--CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
-+CMAKE_MINIMUM_REQUIRED(VERSION 2.8.11)  # first version TARGET_INCLUDE_DIRECTORIES
- 
- PROJECT(LIGHTSD C)
- 
-@@ -22,28 +22,34 @@
- # TODO: we need at least 2.0.19-stable because of the logging defines
- FIND_PACKAGE(Event2 REQUIRED COMPONENTS core)
- FIND_PACKAGE(Endian REQUIRED)
-+
-+INCLUDE(CheckFunctionExists)
- INCLUDE(TestBigEndian)
-+INCLUDE(CompatSetProctitle)
- INCLUDE(CompatTimeMonotonic)
- 
--TEST_BIG_ENDIAN(LGTD_BIG_ENDIAN_SYSTEM)
-+TEST_BIG_ENDIAN(BIG_ENDIAN_SYSTEM)
- 
- ### Global definitions #########################################################
- 
- INCLUDE(AddAllSubdirectories)
- INCLUDE(AddTestFromSources)
- 
--SET(CMAKE_C_FLAGS "-pipe -Wextra -Wall -Wstrict-prototypes -std=c99")
-+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -Wextra -Wall -Wstrict-prototypes -std=c99")
- 
--ADD_DEFINITIONS("-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}")
--ADD_DEFINITIONS("-DLGTD_BIG_ENDIAN_SYSTEM=${LGTD_BIG_ENDIAN_SYSTEM}")
--
--# Only relevant for the GNU libc:
- ADD_DEFINITIONS(
-+    # Only relevant for the GNU libc:
-     "-D_POSIX_C_SOURCE=200809L"
-     "-D_BSD_SOURCE=1"
-     "-D_DEFAULT_SOURCE=1"
- 
-     "-D_DARWIN_C_SOURCE=1"
-+
-+    "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}"
-+    "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}"
-+
-+    "-DLGTD_HAVE_LIBBSD=${HAVE_LIBBSD}"
-+    "-DLGTD_HAVE_PROCTITLE=${HAVE_PROCTITLE}"
- )
- 
- IF (CMAKE_BUILD_TYPE MATCHES "DEBUG")
-@@ -54,10 +60,11 @@
- ENDIF ()
- 
- INCLUDE_DIRECTORIES(
--    ${LIGHTSD_SOURCE_DIR}/compat/generic
-     ${LIGHTSD_BINARY_DIR}/compat
-+    ${LIGHTSD_BINARY_DIR}/compat/generic
- )
- 
-+ADD_SUBDIRECTORY(compat)
- ADD_SUBDIRECTORY(core)
- ADD_SUBDIRECTORY(lifx)
- ADD_SUBDIRECTORY(tests)
-diff --git a/CMakeScripts/AddTestFromSources.cmake b/CMakeScripts/AddTestFromSources.cmake
---- a/CMakeScripts/AddTestFromSources.cmake
-+++ b/CMakeScripts/AddTestFromSources.cmake
-@@ -1,11 +1,11 @@
--FUNCTION(ADD_TEST_FROM_C_SOURCES TEST_SOURCE TEST_LIB)
-+FUNCTION(ADD_TEST_FROM_C_SOURCES TEST_SOURCE)
-     STRING(LENGTH ${TEST_SOURCE} TEST_NAME_LEN)
-     STRING(LENGTH "test_" PREFIX_LEN)
-     MATH(EXPR TEST_NAME_LEN "${TEST_NAME_LEN} - 2 - ${PREFIX_LEN}")
-     STRING(SUBSTRING ${ARGV0} ${PREFIX_LEN} ${TEST_NAME_LEN} TEST_NAME)
--    ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCE} ${ARGN})
--    IF (TEST_LIB)
--        TARGET_LINK_LIBRARIES(${TEST_NAME} ${TEST_LIB})
-+    ADD_EXECUTABLE(${TEST_NAME} ${TEST_SOURCE})
-+    IF (ARGN)
-+        TARGET_LINK_LIBRARIES(${TEST_NAME} ${ARGN})
-     ENDIF ()
-     ADD_TEST(test_${TEST_NAME} ${TEST_NAME})
- ENDFUNCTION()
-diff --git a/CMakeScripts/CompatSetProctitle.cmake b/CMakeScripts/CompatSetProctitle.cmake
-new file mode 100644
---- /dev/null
-+++ b/CMakeScripts/CompatSetProctitle.cmake
-@@ -0,0 +1,20 @@
-+IF (NOT HAVE_PROCTITLE)
-+    SET(CMAKE_REQUIRED_QUIET TRUE)
-+    SET(HAVE_PROCTITLE 0 CACHE INTERNAL "setproctitle found in libbsd")
-+    SET(HAVE_LIBBSD 0 CACHE INTERNAL "libbsd found")
-+    MESSAGE(STATUS "Looking for setproctitle")
-+    CHECK_FUNCTION_EXISTS("setproctitle" HAVE_PROCTITLE)
-+    IF (NOT HAVE_PROCTITLE)
-+        MESSAGE(STATUS "Looking for setproctitle - not found, falling back on libbsd")
-+        FIND_PACKAGE(LibBSD)
-+        IF (NOT LibBSD_FOUND)
-+            MESSAGE(STATUS "Couldn't find setproctitle, no fancy report in the process list")
-+        ELSE ()
-+            SET(HAVE_PROCTITLE 1 CACHE INTERNAL "setproctitle found in libbsd")
-+            SET(HAVE_LIBBSD 1 CACHE INTERNAL "libbsd found")
-+        ENDIF ()
-+    ELSE ()
-+        SET(HAVE_PROCTITLE 1 CACHE INTERNAL "setproctitle found on the system")
-+    ENDIF ()
-+    UNSET(CMAKE_REQUIRED_QUIET)
-+ENDIF ()
-diff --git a/CMakeScripts/CompatTimeMonotonic.cmake b/CMakeScripts/CompatTimeMonotonic.cmake
---- a/CMakeScripts/CompatTimeMonotonic.cmake
-+++ b/CMakeScripts/CompatTimeMonotonic.cmake
-@@ -1,5 +1,3 @@
--INCLUDE(CheckFunctionExists)
--
- IF (NOT TIME_MONOTONIC_LIBRARY)
-     SET(COMPAT_TIME_MONOTONIC_IMPL "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.c")
-     SET(COMPAT_TIME_MONOTONIC_H "${LIGHTSD_SOURCE_DIR}/compat/${CMAKE_SYSTEM_NAME}/time_monotonic.h")
-diff --git a/CMakeScripts/FindLibBSD.cmake b/CMakeScripts/FindLibBSD.cmake
-new file mode 100644
---- /dev/null
-+++ b/CMakeScripts/FindLibBSD.cmake
-@@ -0,0 +1,10 @@
-+FIND_PATH(LIBBSD_INCLUDE_DIR bsd.h PATH_SUFFIXES bsd)
-+
-+FIND_LIBRARY(LIBBSD_LIBRARY bsd)
-+IF(LIBBSD_LIBRARY)
-+    SET(LibBSD_FOUND TRUE)
-+ENDIF()
-+
-+INCLUDE(FindPackageHandleStandardArgs)
-+
-+FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibBSD DEFAULT_MSG LIBBSD_LIBRARY LIBBSD_INCLUDE_DIR)
-diff --git a/README.rst b/README.rst
---- a/README.rst
-+++ b/README.rst
-@@ -65,6 +65,9 @@
- - CMake ≥ 2.8;
- - libevent ≥ 2.0.19.
- 
-+lightsd optionally depends on libbsd ≥ 0.5.0 on platforms missing
-+``setproctitle`` (pretty much any non-BSD system, including Mac OS X).
-+
- lightsd is actively developed and tested from Arch Linux, Debian and Mac OS X;
- both for 32/64 bits and little/big endian architectures.
- 
-@@ -86,4 +89,19 @@
- 
-    .../lightsd/build$ core/lightsd -v info -l ::1:1234
- 
-+lightsd forks in the background by default, display running processes and check
-+how we are doing:
-+
-+::
-+
-+   ps aux | grep lightsd
-+
-+You can stop lightsd with:
-+
-+::
-+
-+   pkill lightsd
-+
-+Use the ``-f`` option to run lightsd in the foreground.
-+
- .. vim: set tw=80 spelllang=en spell:
-diff --git a/compat/CMakeLists.txt b/compat/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/compat/CMakeLists.txt
-@@ -0,0 +1,1 @@
-+ADD_SUBDIRECTORY(generic)
-diff --git a/compat/generic/CMakeLists.txt b/compat/generic/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/compat/generic/CMakeLists.txt
-@@ -0,0 +1,1 @@
-+FILE(COPY sys DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
-diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
---- a/core/CMakeLists.txt
-+++ b/core/CMakeLists.txt
-@@ -15,6 +15,7 @@
- ADD_EXECUTABLE(
-     lightsd
-     client.c
-+    daemon.c
-     jsmn.c
-     jsonrpc.c
-     listen.c
-@@ -23,6 +24,7 @@
-     pipe.c
-     proto.c
-     router.c
-+    stats.c
- )
- 
- TARGET_LINK_LIBRARIES(
-@@ -31,3 +33,7 @@
-     ${EVENT2_CORE_LIBRARY}
-     ${TIME_MONOTONIC_LIBRARY}
- )
-+
-+IF (HAVE_LIBBSD)
-+    TARGET_LINK_LIBRARIES(lightsd ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
-diff --git a/core/client.c b/core/client.c
---- a/core/client.c
-+++ b/core/client.c
-@@ -34,6 +34,8 @@
- #include "jsonrpc.h"
- #include "client.h"
- #include "proto.h"
-+#include "stats.h"
-+#include "daemon.h"
- #include "lightsd.h"
- 
- struct lgtd_client_list lgtd_clients = LIST_HEAD_INITIALIZER(&lgtd_clients);
-@@ -44,6 +46,8 @@
-     assert(client);
-     assert(client->io);
- 
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, -1);
-+
-     LIST_REMOVE(client, link);
-     bufferevent_free(client->io);
-     free(client);
-@@ -217,6 +221,8 @@
- 
-     LIST_INSERT_HEAD(&lgtd_clients, client, link);
- 
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1);
-+
-     return client;
- }
- 
-diff --git a/core/daemon.c b/core/daemon.c
-new file mode 100644
---- /dev/null
-+++ b/core/daemon.c
-@@ -0,0 +1,155 @@
-+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
-+//
-+// This file is part of lighstd.
-+//
-+// lighstd is free software: you can redistribute it and/or modify
-+// it under the terms of the GNU General Public License as published by
-+// the Free Software Foundation, either version 3 of the License, or
-+// (at your option) any later version.
-+//
-+// lighstd is distributed in the hope that it will be useful,
-+// but WITHOUT ANY WARRANTY; without even the implied warranty of
-+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-+// GNU General Public License for more details.
-+//
-+// You should have received a copy of the GNU General Public License
-+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
-+
-+#include <sys/queue.h>
-+#include <sys/tree.h>
-+#include <sys/types.h>
-+#include <endian.h>
-+#include <fcntl.h>
-+#include <stdbool.h>
-+#include <stdint.h>
-+#include <stdio.h>
-+#include <stdlib.h>
-+#include <string.h>
-+#include <unistd.h>
-+
-+#if LGTD_HAVE_LIBBSD
-+#include <bsd/bsd.h>
-+#endif
-+
-+#include <event2/util.h>
-+
-+#include "time_monotonic.h"
-+#include "lifx/wire_proto.h"
-+#include "lifx/bulb.h"
-+#include "lifx/gateway.h"
-+#include "jsmn.h"
-+#include "jsonrpc.h"
-+#include "client.h"
-+#include "listen.h"
-+#include "daemon.h"
-+#include "pipe.h"
-+#include "stats.h"
-+#include "lightsd.h"
-+
-+bool
-+lgtd_daemon_unleash(void)
-+{
-+    if (chdir("/")) {
-+        return false;
-+    }
-+
-+    int null = open("/dev/null", O_RDWR);
-+    if (null == -1) {
-+        return false;
-+    }
-+
-+    const int fds[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO };
-+    for (int i = 0; i != LGTD_ARRAY_SIZE(fds); ++i) {
-+        if (dup2(null, fds[i]) == -1) {
-+            close(null);
-+            return false;
-+        }
-+    }
-+    close(null);
-+
-+#define SUMMON()  do {        \
-+    switch (fork()) {       \
-+        case 0:             \
-+            break;          \
-+        case -1:            \
-+            return false;   \
-+        default:            \
-+            exit(0);        \
-+    }                       \
-+} while (0)
-+
-+    SUMMON(); // \_o< !
-+    setsid();
-+
-+    SUMMON(); // \_o< !!
-+
-+    return true; // welcome to UNIX!
-+}
-+
-+void
-+lgtd_daemon_setup_proctitle(int argc, char *argv[], char *envp[])
-+{
-+#if LGTD_HAVE_LIBBSD
-+    setproctitle_init(argc, argv, envp);
-+    lgtd_daemon_update_proctitle();
-+#else
-+    (void)argc;
-+    (void)argv;
-+    (void)envp;
-+#endif
-+}
-+
-+void
-+lgtd_daemon_update_proctitle(void)
-+{
-+#if LGTD_HAVE_PROCTITLE
-+    char title[LGTD_DAEMON_TITLE_SIZE] = { 0 };
-+    int i = 0;
-+
-+#define TITLE_APPEND(fmt, ...) do {                                         \
-+    int n = snprintf((&title[i]), (sizeof(title) - i), (fmt), __VA_ARGS__); \
-+    i = LGTD_MIN(i + n, (int)sizeof(title));                                \
-+} while (0)
-+
-+#define PREFIX(fmt, ...) TITLE_APPEND(                              \
-+    "%s" fmt, (i && title[i - 1] == ')' ? "; " : ""), __VA_ARGS__   \
-+)
-+
-+#define ADD_ITEM(fmt, ...) TITLE_APPEND(                            \
-+    "%s" fmt, (i && title[i - 1] != '(' ? ", " : ""), __VA_ARGS__   \
-+)
-+#define LOOP(list_type, list, elem_type, prefix, ...) do {    \
-+    if (!list_type ## _EMPTY(list)) {                         \
-+        PREFIX("%s(", prefix);                                \
-+        elem_type *it;                                        \
-+        list_type ## _FOREACH(it, list, link) {               \
-+            ADD_ITEM(__VA_ARGS__);                            \
-+        }                                                     \
-+        TITLE_APPEND("%s", ")");                              \
-+    }                                                         \
-+} while (0)
-+
-+    LOOP(
-+        SLIST, &lgtd_listeners, struct lgtd_listen,
-+        "listening_on", "%s:[%s]", it->addr, it->port
-+    );
-+
-+    LOOP(
-+        SLIST, &lgtd_command_pipes, struct lgtd_command_pipe,
-+        "command_pipes", "%s", it->path
-+    );
-+
-+    if (!LIST_EMPTY(&lgtd_lifx_gateways)) {
-+        PREFIX("lifx_gateways(found=%d)", LGTD_STATS_GET(gateways));
-+    }
-+
-+    PREFIX(
-+        "bulbs(found=%d, on=%d)",
-+        LGTD_STATS_GET(bulbs), LGTD_STATS_GET(bulbs_powered_on)
-+    );
-+
-+    PREFIX("clients(connected=%d)", LGTD_STATS_GET(clients));
-+
-+    setproctitle("%s", title);
-+#endif
-+}
-diff --git a/core/daemon.h b/core/daemon.h
-new file mode 100644
---- /dev/null
-+++ b/core/daemon.h
-@@ -0,0 +1,24 @@
-+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
-+//
-+// This file is part of lighstd.
-+//
-+// lighstd is free software: you can redistribute it and/or modify
-+// it under the terms of the GNU General Public License as published by
-+// the Free Software Foundation, either version 3 of the License, or
-+// (at your option) any later version.
-+//
-+// lighstd is distributed in the hope that it will be useful,
-+// but WITHOUT ANY WARRANTY; without even the implied warranty of
-+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-+// GNU General Public License for more details.
-+//
-+// You should have received a copy of the GNU General Public License
-+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
-+
-+#pragma once
-+
-+enum { LGTD_DAEMON_TITLE_SIZE = 2048 };
-+
-+bool lgtd_daemon_unleash(void); // \_o<
-+void lgtd_daemon_setup_proctitle(int, char *[], char *[]);
-+void lgtd_daemon_update_proctitle(void);
-diff --git a/core/lightsd.c b/core/lightsd.c
---- a/core/lightsd.c
-+++ b/core/lightsd.c
-@@ -47,6 +47,7 @@
- #include "client.h"
- #include "pipe.h"
- #include "listen.h"
-+#include "daemon.h"
- #include "lightsd.h"
- 
- struct lgtd_opts lgtd_opts = {
-@@ -142,8 +143,10 @@
- }
- 
- int
--main(int argc, char *argv[])
-+main(int argc, char *argv[], char *envp[])
- {
-+    lgtd_daemon_setup_proctitle(argc, argv, envp);
-+
-     lgtd_configure_libevent();
-     lgtd_configure_signal_handling();
- 
-@@ -217,6 +220,10 @@
-         lgtd_err(1, "can't setup lightsd");
-     }
- 
-+    if (!lgtd_opts.foreground && !lgtd_daemon_unleash()) {
-+        lgtd_err(1, "can't fork to the background");
-+    }
-+
-     lgtd_lifx_timer_start_discovery();
- 
-     event_base_dispatch(lgtd_ev_base);
-diff --git a/core/listen.c b/core/listen.c
---- a/core/listen.c
-+++ b/core/listen.c
-@@ -30,6 +30,7 @@
- #include "jsonrpc.h"
- #include "client.h"
- #include "listen.h"
-+#include "daemon.h"
- #include "lightsd.h"
- 
- struct lgtd_listen_list lgtd_listeners =
-@@ -69,6 +70,8 @@
-         evconnlistener_free(listener->evlistener);
-         free(listener);
-     }
-+
-+    lgtd_daemon_update_proctitle();
- }
- 
- bool
-@@ -130,6 +133,8 @@
- 
-     evutil_freeaddrinfo(res);
- 
-+    lgtd_daemon_update_proctitle();
-+
-     return true;
- 
- error:
-diff --git a/core/listen.h b/core/listen.h
---- a/core/listen.h
-+++ b/core/listen.h
-@@ -17,6 +17,8 @@
- 
- #pragma once
- 
-+struct evconnlistener;
-+
- struct lgtd_listen {
-     SLIST_ENTRY(lgtd_listen)    link;
-     const char                  *addr;
-@@ -25,5 +27,7 @@
- };
- SLIST_HEAD(lgtd_listen_list, lgtd_listen);
- 
-+extern struct lgtd_listen_list lgtd_listeners;
-+
- bool lgtd_listen_open(const char *, const char *);
- void lgtd_listen_close_all(void);
-diff --git a/core/log.c b/core/log.c
---- a/core/log.c
-+++ b/core/log.c
-@@ -28,9 +28,14 @@
- #include <stdio.h>
- #include <time.h>
- 
-+#if LGTD_HAVE_LIBBSD
-+#include <bsd/unistd.h>
-+#endif
-+
- #include <event2/event.h>
- 
- #include "lifx/wire_proto.h"
-+#include "stats.h"
- #include "lightsd.h"
- 
- static void
-diff --git a/core/stats.c b/core/stats.c
-new file mode 100644
---- /dev/null
-+++ b/core/stats.c
-@@ -0,0 +1,47 @@
-+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
-+//
-+// This file is part of lighstd.
-+//
-+// lighstd is free software: you can redistribute it and/or modify
-+// it under the terms of the GNU General Public License as published by
-+// the Free Software Foundation, either version 3 of the License, or
-+// (at your option) any later version.
-+//
-+// lighstd is distributed in the hope that it will be useful,
-+// but WITHOUT ANY WARRANTY; without even the implied warranty of
-+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-+// GNU General Public License for more details.
-+//
-+// You should have received a copy of the GNU General Public License
-+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
-+
-+#include <assert.h>
-+#include <stdint.h>
-+
-+#include "stats.h"
-+
-+struct lgtd_stats lgtd_counters = { .gateways = 0 };
-+
-+void
-+lgtd_stats_add(int offset, int value)
-+{
-+    assert(offset >= 0);
-+    assert(offset < (int)sizeof(lgtd_counters));
-+    assert(offset % sizeof(int) == 0);
-+
-+    int *counter = (int *)((uint8_t *)&lgtd_counters + offset);
-+
-+    assert(*counter + value >= 0);
-+
-+    *counter += value;
-+}
-+
-+int
-+lgtd_stats_get(int offset)
-+{
-+    assert(offset >= 0);
-+    assert(offset < (int)sizeof(lgtd_counters));
-+    assert(offset % sizeof(int) == 0);
-+
-+    return *(int *)((uint8_t *)&lgtd_counters + offset);
-+}
-diff --git a/core/stats.h b/core/stats.h
-new file mode 100644
---- /dev/null
-+++ b/core/stats.h
-@@ -0,0 +1,35 @@
-+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
-+//
-+// This file is part of lighstd.
-+//
-+// lighstd is free software: you can redistribute it and/or modify
-+// it under the terms of the GNU General Public License as published by
-+// the Free Software Foundation, either version 3 of the License, or
-+// (at your option) any later version.
-+//
-+// lighstd is distributed in the hope that it will be useful,
-+// but WITHOUT ANY WARRANTY; without even the implied warranty of
-+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-+// GNU General Public License for more details.
-+//
-+// You should have received a copy of the GNU General Public License
-+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
-+
-+#pragma once
-+
-+struct lgtd_stats {
-+    int gateways;
-+    int bulbs;
-+    int bulbs_powered_on;
-+    int clients;
-+};
-+
-+void lgtd_stats_add(int, int);
-+int lgtd_stats_get(int);
-+
-+#define LGTD_STATS_GET(name) lgtd_stats_get(offsetof(struct lgtd_stats, name))
-+
-+#define LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(name, value) do {           \
-+    lgtd_stats_add(offsetof(struct lgtd_stats, name), (value));         \
-+    lgtd_daemon_update_proctitle();                                     \
-+} while (0)
-diff --git a/lifx/bulb.c b/lifx/bulb.c
---- a/lifx/bulb.c
-+++ b/lifx/bulb.c
-@@ -32,6 +32,8 @@
- #include "core/time_monotonic.h"
- #include "bulb.h"
- #include "gateway.h"
-+#include "core/daemon.h"
-+#include "core/stats.h"
- #include "core/lightsd.h"
- 
- struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table =
-@@ -62,6 +64,7 @@
-     bulb->gw = gw;
-     memcpy(bulb->addr, addr, sizeof(bulb->addr));
-     RB_INSERT(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb);
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, 1);
- 
-     bulb->last_light_state_at = lgtd_time_monotonic_msecs();
- 
-@@ -74,6 +77,10 @@
-     assert(bulb);
-     assert(bulb->gw);
- 
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, -1);
-+    if (bulb->state.power == LGTD_LIFX_POWER_ON) {
-+        LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1);
-+    }
-     RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb);
-     SLIST_REMOVE(&bulb->gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw);
-     lgtd_info(
-@@ -94,6 +101,13 @@
- {
-     assert(bulb);
-     assert(state);
-+
-+    if (state->power != bulb->state.power) {
-+        LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(
-+            bulbs_powered_on, state->power == LGTD_LIFX_POWER_ON ? 1 : -1
-+        );
-+    }
-+
-     bulb->last_light_state_at = received_at;
-     memcpy(&bulb->state, state, sizeof(bulb->state));
- }
-@@ -102,5 +116,12 @@
- lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *bulb, uint16_t power)
- {
-     assert(bulb);
-+
-+    if (power != bulb->state.power) {
-+        LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(
-+            bulbs_powered_on, power == LGTD_LIFX_POWER_ON ? 1 : -1
-+        );
-+    }
-+
-     bulb->state.power = power;
- }
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -44,6 +44,8 @@
- #include "core/client.h"
- #include "core/proto.h"
- #include "core/router.h"
-+#include "core/stats.h"
-+#include "core/daemon.h"
- #include "core/lightsd.h"
- 
- struct lgtd_lifx_gateway_list lgtd_lifx_gateways =
-@@ -54,6 +56,7 @@
- {
-     assert(gw);
- 
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, -1);
-     event_del(gw->refresh_ev);
-     event_del(gw->write_ev);
-     if (gw->socket != -1) {
-@@ -284,6 +287,8 @@
-     // will stop by itself:
-     lgtd_lifx_timer_start_watchdog();
- 
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1);
-+
-     return gw;
- 
- error_allocate:
-diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
---- a/tests/CMakeLists.txt
-+++ b/tests/CMakeLists.txt
-@@ -1,1 +1,14 @@
-+FUNCTION(ADD_CORE_LIBRARY LIBNAME)
-+    ADD_LIBRARY(${LIBNAME} ${ARGN})
-+    TARGET_LINK_LIBRARIES(${LIBNAME} ${TIME_MONOTONIC_LIBRARY})
-+    TARGET_INCLUDE_DIRECTORIES(
-+        ${LIBNAME} PUBLIC
-+        ${LIGHTSD_SOURCE_DIR}/core/
-+        ${LIGHTSD_BINARY_DIR}/core/
-+    )
-+    IF (HAVE_LIBBSD)
-+        TARGET_LINK_LIBRARIES(${LIBNAME} ${LIBBSD_LIBRARY})
-+    ENDIF (HAVE_LIBBSD)
-+ENDFUNCTION()
-+
- ADD_ALL_SUBDIRECTORIES()
-diff --git a/tests/core/daemon/CMakeLists.txt b/tests/core/daemon/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/tests/core/daemon/CMakeLists.txt
-@@ -0,0 +1,24 @@
-+INCLUDE_DIRECTORIES(
-+    ${CMAKE_CURRENT_SOURCE_DIR}
-+    ${CMAKE_CURRENT_BINARY_DIR}
-+)
-+
-+ADD_CORE_LIBRARY(
-+    test_core_daemon STATIC
-+    ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
-+)
-+
-+FUNCTION(ADD_DAEMON_TEST TEST_SOURCE)
-+    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_daemon)
-+ENDFUNCTION()
-+
-+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
-+FOREACH(TEST ${TESTS})
-+    ADD_DAEMON_TEST(${TEST})
-+ENDFOREACH()
-diff --git a/tests/core/daemon/mock_pipe.h b/tests/core/daemon/mock_pipe.h
-new file mode 100644
---- /dev/null
-+++ b/tests/core/daemon/mock_pipe.h
-@@ -0,0 +1,4 @@
-+#pragma once
-+
-+struct lgtd_command_pipe_list lgtd_command_pipes =
-+    SLIST_HEAD_INITIALIZER(&lgtd_command_pipes);
-diff --git a/tests/core/daemon/test_daemon_update_proctitle.c b/tests/core/daemon/test_daemon_update_proctitle.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/daemon/test_daemon_update_proctitle.c
-@@ -0,0 +1,109 @@
-+void mock_setproctitle(const char *fmt, ...)
-+    __attribute__((format(printf, 1, 2)));
-+
-+#undef LGTD_HAVE_LIBBSD
-+#undef LGTD_HAVE_PROCTITLE
-+#define LGTD_HAVE_PROCTITLE 1
-+#define setproctitle mock_setproctitle
-+#include "daemon.c"
-+
-+#include <err.h>
-+
-+#include "mock_gateway.h"
-+#include "mock_pipe.h"
-+
-+#include "tests_utils.h"
-+
-+const char *expected = "";
-+int setproctitle_call_count = 0;
-+
-+void
-+mock_setproctitle(const char *fmt, ...)
-+{
-+    if (strcmp(fmt, "%s")) {
-+        errx(1, "unexepected format %s (expected %%s)", fmt);
-+    }
-+
-+    va_list ap;
-+    va_start(ap, fmt);
-+    const char *title = va_arg(ap, const char *);
-+    va_end(ap);
-+
-+    if (strcmp(title, expected)) {
-+        errx(1, "unexpected title: %s (expected %s)", title, expected);
-+    }
-+
-+    setproctitle_call_count++;
-+}
-+
-+int
-+main(void)
-+{
-+    expected = "bulbs(found=0, on=0); clients(connected=0)";
-+    lgtd_daemon_update_proctitle();
-+    if (setproctitle_call_count != 1) {
-+        errx(1, "setproctitle should have been called");
-+    }
-+
-+    expected = (
-+        "lifx_gateways(found=1); "
-+        "bulbs(found=0, on=0); "
-+        "clients(connected=0)"
-+    );
-+    struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1);
-+    if (setproctitle_call_count != 2) {
-+        errx(1, "setproctitle should have been called");
-+    }
-+
-+    expected = (
-+        "lifx_gateways(found=1); "
-+        "bulbs(found=1, on=0); "
-+        "clients(connected=0)"
-+    );
-+    lgtd_tests_insert_mock_bulb(gw_1, 2);
-+    expected = (
-+        "lifx_gateways(found=1); "
-+        "bulbs(found=2, on=0); "
-+        "clients(connected=0)"
-+    );
-+    lgtd_tests_insert_mock_bulb(gw_1, 3);
-+    if (setproctitle_call_count != 4) {
-+        errx(1, "setproctitle should have been called");
-+    }
-+
-+    expected = (
-+        "listening_on(foobar.com:[1234]); "
-+        "lifx_gateways(found=1); "
-+        "bulbs(found=2, on=0); "
-+        "clients(connected=0)"
-+    );
-+    lgtd_tests_insert_mock_listener("foobar.com", "1234");
-+    lgtd_daemon_update_proctitle();
-+    if (setproctitle_call_count != 5) {
-+        errx(1, "setproctitle should have been called");
-+    }
-+
-+    expected = (
-+        "listening_on(foobar.com:[1234]); "
-+        "lifx_gateways(found=1); "
-+        "bulbs(found=2, on=1); "
-+        "clients(connected=0)"
-+    );
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1);
-+    if (setproctitle_call_count != 6) {
-+        errx(1, "setproctitle should have been called");
-+    }
-+
-+    expected = (
-+        "listening_on(foobar.com:[1234]); "
-+        "lifx_gateways(found=1); "
-+        "bulbs(found=2, on=1); "
-+        "clients(connected=1)"
-+    );
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(clients, 1);
-+    if (setproctitle_call_count != 7) {
-+        errx(1, "setproctitle should have been called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/jsonrpc/CMakeLists.txt b/tests/core/jsonrpc/CMakeLists.txt
---- a/tests/core/jsonrpc/CMakeLists.txt
-+++ b/tests/core/jsonrpc/CMakeLists.txt
-@@ -3,16 +3,16 @@
-     ${CMAKE_CURRENT_BINARY_DIR}
- )
- 
--ADD_LIBRARY(
-+ADD_CORE_LIBRARY(
-     test_core_jsonrpc STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/jsmn.c
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
- )
--TARGET_LINK_LIBRARIES(test_core_jsonrpc ${TIME_MONOTONIC_LIBRARY})
- 
- FUNCTION(ADD_JSONRPC_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_jsonrpc)
-diff --git a/tests/core/mock_daemon.h b/tests/core/mock_daemon.h
-new file mode 100644
---- /dev/null
-+++ b/tests/core/mock_daemon.h
-@@ -0,0 +1,8 @@
-+#pragma once
-+
-+#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE
-+void
-+lgtd_daemon_update_proctitle(void)
-+{
-+}
-+#endif
-diff --git a/tests/core/pipe/CMakeLists.txt b/tests/core/pipe/CMakeLists.txt
---- a/tests/core/pipe/CMakeLists.txt
-+++ b/tests/core/pipe/CMakeLists.txt
-@@ -7,6 +7,7 @@
-     test_core_pipe STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/jsmn.c
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-diff --git a/tests/core/pipe/tests_pipe_utils.h b/tests/core/pipe/tests_pipe_utils.h
---- a/tests/core/pipe/tests_pipe_utils.h
-+++ b/tests/core/pipe/tests_pipe_utils.h
-@@ -1,5 +1,7 @@
- #pragma once
- 
-+#include "mock_daemon.h"
-+
- #ifndef MOCKED_CLIENT_OPEN_FROM_PIPE
- void
- lgtd_client_open_from_pipe(struct lgtd_client *pipe_client)
-diff --git a/tests/core/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt
---- a/tests/core/proto/CMakeLists.txt
-+++ b/tests/core/proto/CMakeLists.txt
-@@ -3,10 +3,11 @@
-     ${CMAKE_CURRENT_BINARY_DIR}
- )
- 
--ADD_LIBRARY(
-+ADD_CORE_LIBRARY(
-     test_core_proto STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-     ${LIGHTSD_SOURCE_DIR}/core/jsonrpc.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/timer.c
-@@ -14,7 +15,6 @@
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
- )
--TARGET_LINK_LIBRARIES(test_core_proto ${TIME_MONOTONIC_LIBRARY})
- 
- FUNCTION(ADD_PROTO_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_proto)
-diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c
---- a/tests/core/proto/test_proto_get_light_state.c
-+++ b/tests/core/proto/test_proto_get_light_state.c
-@@ -1,6 +1,8 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
-+#include "mock_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c
---- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c
-+++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c
---- a/tests/core/proto/test_proto_get_light_state_null_device_list.c
-+++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c
---- a/tests/core/proto/test_proto_power_off.c
-+++ b/tests/core/proto/test_proto_power_off.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-@@ -38,7 +39,7 @@
- lgtd_client_send_response(struct lgtd_client *client, const char *msg)
- {
-     if (!client) {
--        errx(1, "client shouldn't ne NULL");
-+        errx(1, "client shouldn't be NULL");
-     }
- 
-     if (strcmp(msg, "true")) {
-diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c
---- a/tests/core/proto/test_proto_power_off_routing_error.c
-+++ b/tests/core/proto/test_proto_power_off_routing_error.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c
---- a/tests/core/proto/test_proto_power_on.c
-+++ b/tests/core/proto/test_proto_power_on.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c
---- a/tests/core/proto/test_proto_power_on_routing_error.c
-+++ b/tests/core/proto/test_proto_power_on_routing_error.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-diff --git a/tests/core/proto/test_proto_set_light_from_hsbk.c b/tests/core/proto/test_proto_set_light_from_hsbk.c
---- a/tests/core/proto/test_proto_set_light_from_hsbk.c
-+++ b/tests/core/proto/test_proto_set_light_from_hsbk.c
-@@ -3,6 +3,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-diff --git a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c
---- a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c
-+++ b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c
-@@ -3,6 +3,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c
---- a/tests/core/proto/test_proto_set_waveform.c
-+++ b/tests/core/proto/test_proto_set_waveform.c
-@@ -3,6 +3,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c
---- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c
-+++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c
-@@ -3,6 +3,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-diff --git a/tests/core/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt
---- a/tests/core/router/CMakeLists.txt
-+++ b/tests/core/router/CMakeLists.txt
-@@ -3,10 +3,11 @@
-     ${CMAKE_CURRENT_BINARY_DIR}
- )
- 
--ADD_LIBRARY(
-+ADD_CORE_LIBRARY(
-     test_core_router STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-     ${LIGHTSD_SOURCE_DIR}/core/proto.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/tagging.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/timer.c
-@@ -15,11 +16,7 @@
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
- )
- 
--TARGET_LINK_LIBRARIES(
--    test_core_router
--    ${EVENT2_CORE_LIBRARY}
--    ${TIME_MONOTONIC_LIBRARY}
--)
-+TARGET_LINK_LIBRARIES(test_core_router ${EVENT2_CORE_LIBRARY})
- 
- FUNCTION(ADD_ROUTER_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_router)
-diff --git a/tests/core/router/test_router_send_to_broadcast.c b/tests/core/router/test_router_send_to_broadcast.c
---- a/tests/core/router/test_router_send_to_broadcast.c
-+++ b/tests/core/router/test_router_send_to_broadcast.c
-@@ -1,6 +1,8 @@
- #include "router.c"
- 
-+#include "mock_daemon.h"
- #include "tests_utils.h"
-+
- #include "tests_router_utils.h"
- 
- int
-diff --git a/tests/core/router/test_router_send_to_device.c b/tests/core/router/test_router_send_to_device.c
---- a/tests/core/router/test_router_send_to_device.c
-+++ b/tests/core/router/test_router_send_to_device.c
-@@ -1,5 +1,6 @@
- #include "router.c"
- 
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- #include "tests_router_utils.h"
- 
-diff --git a/tests/core/router/test_router_send_to_invalid_targets.c b/tests/core/router/test_router_send_to_invalid_targets.c
---- a/tests/core/router/test_router_send_to_invalid_targets.c
-+++ b/tests/core/router/test_router_send_to_invalid_targets.c
-@@ -1,5 +1,6 @@
- #include "router.c"
- 
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- #include "tests_router_utils.h"
- 
-diff --git a/tests/core/router/test_router_send_to_label.c b/tests/core/router/test_router_send_to_label.c
---- a/tests/core/router/test_router_send_to_label.c
-+++ b/tests/core/router/test_router_send_to_label.c
-@@ -1,5 +1,6 @@
- #include "router.c"
- 
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- #include "tests_router_utils.h"
- 
-diff --git a/tests/core/router/test_router_send_to_tag.c b/tests/core/router/test_router_send_to_tag.c
---- a/tests/core/router/test_router_send_to_tag.c
-+++ b/tests/core/router/test_router_send_to_tag.c
-@@ -1,5 +1,6 @@
- #include "router.c"
- 
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- #include "tests_router_utils.h"
- 
-diff --git a/tests/core/router/test_router_targets_to_devices.c b/tests/core/router/test_router_targets_to_devices.c
---- a/tests/core/router/test_router_targets_to_devices.c
-+++ b/tests/core/router/test_router_targets_to_devices.c
-@@ -1,5 +1,6 @@
- #include "router.c"
- 
-+#include "mock_daemon.h"
- #include "tests_utils.h"
- #include "tests_router_utils.h"
- 
-diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
---- a/tests/core/tests_utils.c
-+++ b/tests/core/tests_utils.c
-@@ -24,6 +24,9 @@
- #include "core/jsonrpc.h"
- #include "core/client.h"
- #include "core/proto.h"
-+#include "core/listen.h"
-+#include "core/daemon.h"
-+#include "core/stats.h"
- #include "lifx/bulb.h"
- #include "lifx/gateway.h"
- #include "tests_utils.h"
-@@ -31,6 +34,9 @@
- struct lgtd_lifx_gateway_list lgtd_lifx_gateways =
-     LIST_HEAD_INITIALIZER(&lgtd_lifx_gateways);
- 
-+struct lgtd_listen_list lgtd_listeners =
-+    SLIST_HEAD_INITIALIZER(&lgtd_listeners);
-+
- struct lgtd_lifx_gateway *
- lgtd_tests_insert_mock_gateway(int id)
- {
-@@ -41,6 +47,8 @@
- 
-     LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link);
- 
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1);
-+
-     return gw;
- }
- 
-@@ -115,6 +123,17 @@
-     return site;
- }
- 
-+struct lgtd_listen *
-+lgtd_tests_insert_mock_listener(const char *addr, const char *port)
-+{
-+    struct lgtd_listen *listener = calloc(1, sizeof(*listener));
-+    listener->addr = addr;
-+    listener->port = port;
-+    SLIST_INSERT_HEAD(&lgtd_listeners, listener, link);
-+
-+    return listener;
-+}
-+
- char *
- lgtd_tests_make_temp_dir(void)
- {
-diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h
---- a/tests/core/tests_utils.h
-+++ b/tests/core/tests_utils.h
-@@ -40,3 +40,4 @@
- struct lgtd_lifx_site *lgtd_tests_add_tag_to_gw(struct lgtd_lifx_tag *,
-                                                 struct lgtd_lifx_gateway *,
-                                                 int);
-+struct lgtd_listen *lgtd_tests_insert_mock_listener(const char *addr, const char *port);
-diff --git a/tests/lifx/gateway/CMakeLists.txt b/tests/lifx/gateway/CMakeLists.txt
---- a/tests/lifx/gateway/CMakeLists.txt
-+++ b/tests/lifx/gateway/CMakeLists.txt
-@@ -3,21 +3,27 @@
-     ${CMAKE_CURRENT_BINARY_DIR}
- )
- 
--ADD_LIBRARY(
--    test_lifx_gateway STATIC
-+ADD_CORE_LIBRARY(
-+    test_lifx_gateway_core STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-     ${LIGHTSD_SOURCE_DIR}/core/proto.c
-     ${LIGHTSD_SOURCE_DIR}/core/router.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-+)
-+
-+ADD_LIBRARY(
-+    test_lifx_gateway STATIC
-     ${LIGHTSD_SOURCE_DIR}/lifx/broadcast.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/timer.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
--    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
- )
--TARGET_LINK_LIBRARIES(test_lifx_gateway ${TIME_MONOTONIC_LIBRARY})
- 
- FUNCTION(ADD_GATEWAY_TEST TEST_SOURCE)
--    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_gateway)
-+    ADD_TEST_FROM_C_SOURCES(
-+        ${TEST_SOURCE} test_lifx_gateway_core test_lifx_gateway
-+    )
- ENDFUNCTION()
- 
- FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
-diff --git a/tests/lifx/tagging/CMakeLists.txt b/tests/lifx/tagging/CMakeLists.txt
---- a/tests/lifx/tagging/CMakeLists.txt
-+++ b/tests/lifx/tagging/CMakeLists.txt
-@@ -6,8 +6,12 @@
- ADD_LIBRARY(
-     test_lifx_tagging STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
- )
-+IF (HAVE_LIBBSD)
-+    TARGET_LINK_LIBRARIES(test_lifx_tagging ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_TAGGING_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_tagging)
-diff --git a/tests/lifx/tests_shims.c b/tests/lifx/tests_shims.c
---- a/tests/lifx/tests_shims.c
-+++ b/tests/lifx/tests_shims.c
-@@ -35,3 +35,8 @@
-         return ntohs(in6_peer->sin6_port);
-     }
- }
-+
-+void
-+lgtd_daemon_update_proctitle(void)
-+{
-+}
-diff --git a/tests/lifx/wire_proto/CMakeLists.txt b/tests/lifx/wire_proto/CMakeLists.txt
---- a/tests/lifx/wire_proto/CMakeLists.txt
-+++ b/tests/lifx/wire_proto/CMakeLists.txt
-@@ -3,14 +3,15 @@
-     ${CMAKE_CURRENT_BINARY_DIR}
- )
- 
--ADD_LIBRARY(
--    test_lifx_wire_proto STATIC
-+ADD_CORE_LIBRARY(
-+    test_lifx_wire_proto_core STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
- )
- 
- FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE)
--    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto)
-+    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto_core)
- ENDFUNCTION()
- 
- FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
--- a/add_tag_and_untag.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3059 +0,0 @@
-# HG changeset patch
-# Parent  803d8ed2122a3e2d6c5f0c6c0bfbbd6b3c5cec1c
-Add initial support for tag (grouping) and untag (ungrouping)
-
-It's a bit rough without an auto-retry mechanism and it doesn't seem to work
-well with the newer bulbs like the LIFX White 800.
-
-diff --git a/README.rst b/README.rst
---- a/README.rst
-+++ b/README.rst
-@@ -29,8 +29,7 @@
- - set_light_from_hsbk;
- - set_waveform (change the light according to a function like SAW or SINE);
- - get_light_state;
--- tag/untag (group/ungroup bulbs together, coming up: need unit & regression
--  tests);
-+- tag/untag (group/ungroup bulbs together);
- - toggle (power on if off and vice-versa, coming up).
- 
- The JSON-RPC interface works on top of TCP/IPv4/v6, Unix sockets (coming up) or
-diff --git a/core/jsonrpc.c b/core/jsonrpc.c
---- a/core/jsonrpc.c
-+++ b/core/jsonrpc.c
-@@ -977,6 +977,90 @@
-     lgtd_proto_target_list_clear(&targets);
- }
- 
-+static void
-+lgtd_jsonrpc_check_and_call_proto_tag_or_untag(struct lgtd_client *client,
-+                                               void (*lgtd_proto_fn)(struct lgtd_client *,
-+                                                          const struct lgtd_proto_target_list *,
-+                                                          const char *))
-+
-+{
-+    struct lgtd_jsonrpc_target_args {
-+        const jsmntok_t *target;
-+        int             target_ntokens;
-+        const jsmntok_t *tag;
-+    } params = { NULL, 0, NULL };
-+    static const struct lgtd_jsonrpc_node schema[] = {
-+        LGTD_JSONRPC_NODE(
-+            "target",
-+            offsetof(struct lgtd_jsonrpc_target_args, target),
-+            offsetof(struct lgtd_jsonrpc_target_args, target_ntokens),
-+            lgtd_jsonrpc_type_string_number_or_array,
-+            false
-+        ),
-+        LGTD_JSONRPC_NODE(
-+            "tag",
-+            offsetof(struct lgtd_jsonrpc_target_args, tag),
-+            -1,
-+            lgtd_jsonrpc_type_string,
-+            false
-+        )
-+    };
-+
-+    struct lgtd_jsonrpc_request *req = client->current_request;
-+    bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema(
-+        &params,
-+        schema,
-+        LGTD_ARRAY_SIZE(schema),
-+        req->params,
-+        req->params_ntokens,
-+        client->json
-+    );
-+    if (!ok) {
-+        lgtd_jsonrpc_send_error(
-+            client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters"
-+        );
-+        return;
-+    }
-+
-+    struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets);
-+    ok = lgtd_jsonrpc_build_target_list(
-+        &targets, client, params.target, params.target_ntokens
-+    );
-+    if (!ok) {
-+        return;
-+    }
-+
-+    char *tag = strndup(
-+        &client->json[params.tag->start], LGTD_JSONRPC_TOKEN_LEN(params.tag)
-+    );
-+    if (!tag) {
-+        lgtd_warn("can't allocate a tag");
-+        lgtd_jsonrpc_send_error(
-+            client, LGTD_JSONRPC_INTERNAL_ERROR, "Can't allocate memory"
-+        );
-+        goto error_strdup;
-+    }
-+
-+    lgtd_proto_fn(client, &targets, tag);
-+
-+    free(tag);
-+
-+error_strdup:
-+    lgtd_proto_target_list_clear(&targets);
-+}
-+
-+static void
-+lgtd_jsonrpc_check_and_call_tag(struct lgtd_client *client)
-+{
-+    return lgtd_jsonrpc_check_and_call_proto_tag_or_untag(client, lgtd_proto_tag);
-+}
-+
-+static void
-+lgtd_jsonrpc_check_and_call_untag(struct lgtd_client *client)
-+{
-+    return lgtd_jsonrpc_check_and_call_proto_tag_or_untag(client, lgtd_proto_untag);
-+}
-+
- void
- lgtd_jsonrpc_dispatch_request(struct lgtd_client *client, int parsed)
- {
-@@ -1001,6 +1085,14 @@
-         LGTD_JSONRPC_METHOD(
-             "get_light_state", 1, // t
-             lgtd_jsonrpc_check_and_call_get_light_state
-+        ),
-+        LGTD_JSONRPC_METHOD(
-+            "tag", 2, // t, tag
-+            lgtd_jsonrpc_check_and_call_tag
-+        ),
-+        LGTD_JSONRPC_METHOD(
-+            "untag", 2, // t, tag
-+            lgtd_jsonrpc_check_and_call_untag
-         )
-     };
- 
-diff --git a/core/proto.c b/core/proto.c
---- a/core/proto.c
-+++ b/core/proto.c
-@@ -224,3 +224,145 @@
- 
-     lgtd_router_device_list_free(devices);
- }
-+
-+void
-+lgtd_proto_tag(struct lgtd_client *client,
-+               const struct lgtd_proto_target_list *targets,
-+               const char *tag_label)
-+{
-+    assert(client);
-+    assert(targets);
-+    assert(tag_label);
-+
-+    struct lgtd_router_device_list *devices;
-+    devices = lgtd_router_targets_to_devices(targets);
-+    if (!devices) {
-+        goto error_tag_alloc;
-+    }
-+
-+    struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label);
-+    if (!tag) {
-+        tag = lgtd_lifx_tagging_allocate_tag(tag_label);
-+        if (!tag) {
-+            goto error_tag_alloc;
-+        }
-+        lgtd_info("created tag [%s]", tag_label);
-+    }
-+
-+    struct lgtd_router_device *device;
-+    struct lgtd_lifx_site *site;
-+
-+    // Loop over the devices and do allocations first, this makes error
-+    // handling easier (since you can't rollback enqueued packets) and build
-+    // the list of affected gateways so we can do SET_TAG_LABELS:
-+    SLIST_FOREACH(device, devices, link) {
-+        struct lgtd_lifx_gateway *gw = device->device->gw;
-+        int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag);
-+        if (tag_id == -1) {
-+            tag_id = lgtd_lifx_gateway_allocate_tag_id(gw, -1, tag_label);
-+            if (tag_id == -1) {
-+                goto error_site_alloc;
-+            }
-+        }
-+    }
-+
-+    // SET_TAG_LABELS, this is idempotent, do it everytime so we can recover
-+    // from any bad state:
-+    LIST_FOREACH(site, &tag->sites, link) {
-+        int tag_id = site->tag_id;
-+        assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS);
-+        struct lgtd_lifx_packet_tag_labels pkt = { .tags = 0 };
-+        pkt.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-+        strncpy(pkt.label, tag_label, sizeof(pkt.label) - 1);
-+        lgtd_lifx_wire_encode_tag_labels(&pkt);
-+        bool enqueued = lgtd_lifx_gateway_send_to_site(
-+            site->gw, LGTD_LIFX_SET_TAG_LABELS, &pkt
-+        );
-+        if (!enqueued) {
-+            goto error_site_alloc;
-+        }
-+        lgtd_info(
-+            "created tag [%s] with id %d on gw [%s]:%hu",
-+            tag_label, tag_id, site->gw->ip_addr, site->gw->port
-+        );
-+    }
-+
-+    // Finally SET_TAGS on the devices:
-+    SLIST_FOREACH(device, devices, link) {
-+        struct lgtd_lifx_bulb *bulb = device->device;
-+        int tag_id = lgtd_lifx_gateway_get_tag_id(bulb->gw, tag);
-+        assert(tag_id > -1 && tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS);
-+        int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-+        if (!(bulb->state.tags & tag_value)) {
-+            struct lgtd_lifx_packet_tags pkt;
-+            pkt.tags = bulb->state.tags | tag_value;
-+            lgtd_lifx_wire_encode_tags(&pkt);
-+            lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt);
-+        }
-+    }
-+
-+    SEND_RESULT(client, true);
-+    goto fini;
-+
-+error_site_alloc:
-+    if (LIST_EMPTY(&tag->sites)) {
-+        lgtd_lifx_tagging_deallocate_tag(tag);
-+    } else { // tagging_decref will deallocate the tag for us:
-+        struct lgtd_lifx_site *next_site;
-+        LIST_FOREACH_SAFE(site, &tag->sites, link, next_site) {
-+            lgtd_lifx_gateway_deallocate_tag_id(site->gw, site->tag_id);
-+        }
-+    }
-+error_tag_alloc:
-+    lgtd_client_send_error(
-+        client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate new tag"
-+    );
-+fini:
-+    lgtd_router_device_list_free(devices);
-+    return;
-+}
-+
-+void
-+lgtd_proto_untag(struct lgtd_client *client,
-+                 const struct lgtd_proto_target_list *targets,
-+                 const char *tag_label)
-+{
-+    assert(client);
-+    assert(targets);
-+    assert(tag_label);
-+
-+    struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label);
-+    if (!tag) {
-+        SEND_RESULT(client, true);
-+        return;
-+    }
-+
-+    struct lgtd_router_device_list *devices = NULL;
-+    devices = lgtd_router_targets_to_devices(targets);
-+    if (!devices) {
-+        lgtd_client_send_error(
-+            client, LGTD_CLIENT_INTERNAL_ERROR, "couldn't allocate memory"
-+        );
-+        return;
-+    }
-+
-+    struct lgtd_router_device *device;
-+    SLIST_FOREACH(device, devices, link) {
-+        struct lgtd_lifx_bulb *bulb = device->device;
-+        struct lgtd_lifx_gateway *gw = bulb->gw;
-+        int tag_id = lgtd_lifx_gateway_get_tag_id(gw, tag);
-+        if (tag_id != -1) {
-+            int tag_value = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-+            if (bulb->state.tags & tag_value) {
-+                struct lgtd_lifx_packet_tags pkt;
-+                pkt.tags = bulb->state.tags & ~tag_value;
-+                lgtd_lifx_wire_encode_tags(&pkt);
-+                lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt);
-+            }
-+        }
-+    }
-+
-+    SEND_RESULT(client, true);
-+
-+    lgtd_router_device_list_free(devices);
-+}
-diff --git a/core/proto.h b/core/proto.h
---- a/core/proto.h
-+++ b/core/proto.h
-@@ -39,3 +39,5 @@
- void lgtd_proto_power_on(struct lgtd_client *, const struct lgtd_proto_target_list *);
- void lgtd_proto_power_off(struct lgtd_client *, const struct lgtd_proto_target_list *);
- void lgtd_proto_get_light_state(struct lgtd_client *, const struct lgtd_proto_target_list *);
-+void lgtd_proto_tag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *);
-+void lgtd_proto_untag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *);
-diff --git a/core/router.c b/core/router.c
---- a/core/router.c
-+++ b/core/router.c
-@@ -372,8 +372,8 @@
- void
- lgtd_router_device_list_free(struct lgtd_router_device_list *devices)
- {
--    assert(devices);
--
--    lgtd_router_clear_device_list(devices);
--    free(devices);
-+    if (devices) {
-+        lgtd_router_clear_device_list(devices);
-+        free(devices);
-+    }
- }
-diff --git a/examples/lightsc.py b/examples/lightsc.py
---- a/examples/lightsc.py
-+++ b/examples/lightsc.py
-@@ -72,6 +72,14 @@
- def get_light_state(socket, target):
-     return jsonrpc_call(socket, "get_light_state", [target])
- 
-+
-+def tag(socket, target, tag):
-+    return jsonrpc_call(socket, "tag", [target, tag])
-+
-+
-+def untag(socket, target, tag):
-+    return jsonrpc_call(socket, "untag", [target, tag])
-+
- if __name__ == "__main__":
-     s = socket.create_connection(("localhost", 1234))
-     h = 0
-diff --git a/lifx/bulb.c b/lifx/bulb.c
---- a/lifx/bulb.c
-+++ b/lifx/bulb.c
-@@ -77,12 +77,29 @@
-     assert(bulb);
-     assert(bulb->gw);
- 
-+#ifndef NDEBUG
-+    // FIXME: Yeah, so an unit test lgtd_lifx_gateway_remove_and_close_bulb
-+    // would be better because it can be automated, but this looks so much
-+    // easier to do and this code path is often exercised:
-+    int tag_id;
-+    LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) {
-+        int n = 0;
-+        struct lgtd_lifx_bulb *gw_bulb;
-+        SLIST_FOREACH(gw_bulb, &bulb->gw->bulbs, link_by_gw) {
-+            assert(gw_bulb != bulb);
-+            if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & gw_bulb->state.tags) {
-+                n++;
-+            }
-+        }
-+        assert(bulb->gw->tag_refcounts[tag_id] == n);
-+    }
-+#endif
-+
-     LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs, -1);
-     if (bulb->state.power == LGTD_LIFX_POWER_ON) {
-         LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1);
-     }
-     RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb);
--    SLIST_REMOVE(&bulb->gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw);
-     lgtd_info(
-         "closed bulb \"%.*s\" (%s) on [%s]:%hu",
-         LGTD_LIFX_LABEL_SIZE,
-@@ -108,6 +125,8 @@
-         );
-     }
- 
-+    lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, state->tags);
-+
-     bulb->last_light_state_at = received_at;
-     memcpy(&bulb->state, state, sizeof(bulb->state));
- }
-@@ -125,3 +144,13 @@
- 
-     bulb->state.power = power;
- }
-+
-+void
-+lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *bulb, uint64_t tags)
-+{
-+    assert(bulb);
-+
-+    lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, tags);
-+
-+    bulb->state.tags = tags;
-+}
-diff --git a/lifx/bulb.h b/lifx/bulb.h
---- a/lifx/bulb.h
-+++ b/lifx/bulb.h
-@@ -68,3 +68,4 @@
-                                     const struct lgtd_lifx_light_state *,
-                                     lgtd_time_mono_t);
- void lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *, uint16_t);
-+void lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *, uint64_t);
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -71,9 +71,9 @@
-             lgtd_lifx_tagging_decref(gw->tags[i], gw);
-         }
-     }
--    struct lgtd_lifx_bulb *bulb, *next_bulb;
--    SLIST_FOREACH_SAFE(bulb, &gw->bulbs, link_by_gw, next_bulb) {
--        lgtd_lifx_bulb_close(bulb);
-+    while (!SLIST_EMPTY(&gw->bulbs)) {
-+        struct lgtd_lifx_bulb *bulb = SLIST_FIRST(&gw->bulbs);
-+        lgtd_lifx_gateway_remove_and_close_bulb(gw, bulb);
-     }
- 
-     lgtd_info(
-@@ -83,6 +83,23 @@
-     free(gw);
- }
- 
-+void
-+lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *gw,
-+                                        struct lgtd_lifx_bulb *bulb)
-+{
-+    assert(gw);
-+    assert(bulb);
-+
-+    int tag_id;
-+    LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) {
-+        assert(gw->tag_refcounts[tag_id] > 0);
-+        gw->tag_refcounts[tag_id]--;
-+    }
-+    SLIST_REMOVE(&gw->bulbs, bulb, lgtd_lifx_bulb, link_by_gw);
-+
-+    lgtd_lifx_bulb_close(bulb);
-+}
-+
- static void
- lgtd_lifx_gateway_write_callback(evutil_socket_t socket,
-                                  short events, void *ctx)
-@@ -133,13 +150,6 @@
-             if (type == LGTD_LIFX_GET_TAG_LABELS) {
-                 gw->pending_refresh_req = false;
-             }
--            if (lgtd_opts.verbosity <= LGTD_DEBUG) {
--                const struct lgtd_lifx_packet_infos *pkt_infos =
--                    lgtd_lifx_wire_get_packet_infos(type);
--                lgtd_debug(
--                    "%s --> [%s]:%hu", pkt_infos->name, gw->ip_addr, gw->port
--                );
--            }
-             gw->pkt_ring[gw->pkt_ring_tail].type = 0;
-             LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(gw->pkt_ring_tail);
-             gw->pkt_ring_full = false;
-@@ -151,36 +161,77 @@
-     }
- }
- 
-+static bool
-+lgtd_lifx_gateway_send_to_site_impl(struct lgtd_lifx_gateway *gw,
-+                                    enum lgtd_lifx_packet_type pkt_type,
-+                                    const void *pkt,
-+                                    const struct lgtd_lifx_packet_infos **pkt_infos)
-+{
-+    assert(gw);
-+    assert(pkt_infos);
-+
-+    struct lgtd_lifx_packet_header hdr;
-+    union lgtd_lifx_target target = { .addr = gw->site.as_array };
-+    *pkt_infos = lgtd_lifx_wire_setup_header(
-+        &hdr,
-+        LGTD_LIFX_TARGET_SITE,
-+        target,
-+        gw->site.as_array,
-+        pkt_type
-+    );
-+    assert(*pkt_infos);
-+
-+    lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_type, pkt, (*pkt_infos)->size);
-+
-+    return true; // FIXME, have real return values on the send paths...
-+}
-+
-+static bool
-+lgtd_lifx_gateway_send_to_site_quiet(struct lgtd_lifx_gateway *gw,
-+                                     enum lgtd_lifx_packet_type pkt_type,
-+                                     const void *pkt)
-+{
-+
-+    const struct lgtd_lifx_packet_infos *pkt_infos;
-+    bool rv = lgtd_lifx_gateway_send_to_site_impl(
-+        gw, pkt_type, pkt, &pkt_infos
-+    );
-+
-+    lgtd_debug(
-+        "sending %s to site %s",
-+        pkt_infos->name, lgtd_addrtoa(gw->site.as_array)
-+    );
-+
-+    return rv; // FIXME, have real return values on the send paths...
-+}
-+
-+bool
-+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-+                               enum lgtd_lifx_packet_type pkt_type,
-+                               const void *pkt)
-+{
-+    const struct lgtd_lifx_packet_infos *pkt_infos;
-+    bool rv = lgtd_lifx_gateway_send_to_site_impl(
-+        gw, pkt_type, pkt, &pkt_infos
-+    );
-+
-+    lgtd_info(
-+        "sending %s to site %s",
-+        pkt_infos->name, lgtd_addrtoa(gw->site.as_array)
-+    );
-+
-+    return rv; // FIXME, have real return values on the send paths...
-+}
-+
- static void
- lgtd_lifx_gateway_send_get_all_light_state(struct lgtd_lifx_gateway *gw)
- {
-     assert(gw);
- 
--    struct lgtd_lifx_packet_header hdr;
--    union lgtd_lifx_target target = { .addr = gw->site.as_array };
-+    lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_LIGHT_STATE, NULL);
- 
--    lgtd_lifx_wire_setup_header(
--        &hdr,
--        LGTD_LIFX_TARGET_SITE,
--        target,
--        gw->site.as_array,
--        LGTD_LIFX_GET_LIGHT_STATE
--    );
--    lgtd_lifx_gateway_enqueue_packet(
--        gw, &hdr, LGTD_LIFX_GET_LIGHT_STATE, NULL, 0
--    );
--
--    struct lgtd_lifx_packet_get_tag_labels pkt = { .tags = LGTD_LIFX_ALL_TAGS };
--    lgtd_lifx_wire_setup_header(
--        &hdr,
--        LGTD_LIFX_TARGET_SITE,
--        target,
--        gw->site.as_array,
--        LGTD_LIFX_GET_TAG_LABELS
--    );
--    lgtd_lifx_gateway_enqueue_packet(
--        gw, &hdr, LGTD_LIFX_GET_TAG_LABELS, &pkt, sizeof(pkt)
--    );
-+    struct lgtd_lifx_packet_tags pkt = { .tags = LGTD_LIFX_ALL_TAGS };
-+    lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_TAG_LABELS, &pkt);
- 
-     gw->pending_refresh_req = true;
- }
-@@ -371,19 +422,55 @@
- }
- 
- void
-+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw,
-+                                       uint64_t bulb_tags,
-+                                       uint64_t pkt_tags)
-+{
-+    uint64_t changes = bulb_tags ^ pkt_tags;
-+    uint64_t added_tags = changes & pkt_tags;
-+    uint64_t removed_tags = changes & bulb_tags;
-+    int tag_id;
-+
-+    LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, added_tags) {
-+        if (gw->tag_refcounts[tag_id] != UINT8_MAX) {
-+            gw->tag_refcounts[tag_id]++;
-+        } else {
-+            lgtd_warnx(
-+                "reached refcount limit (%u) for tag [%s] (%d) on gw [%s]:%hu",
-+                UINT8_MAX, gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL,
-+                tag_id, gw->ip_addr, gw->port
-+            );
-+        }
-+    }
-+
-+    LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, removed_tags) {
-+        assert(gw->tag_refcounts[tag_id] > 0);
-+        if (--gw->tag_refcounts[tag_id] == 0) {
-+            lgtd_info(
-+                "deleting unused tag [%s] (%d) from gw [%s]:%hu (site %s)",
-+                gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL, tag_id,
-+                gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array)
-+            );
-+            struct lgtd_lifx_packet_tag_labels pkt = {
-+                .tags = ~(gw->tag_ids & ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))
-+            };
-+            lgtd_lifx_wire_encode_tag_labels(&pkt);
-+            lgtd_lifx_gateway_send_to_site(gw, LGTD_LIFX_SET_TAG_LABELS, &pkt);
-+        }
-+    }
-+}
-+
-+void
- lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw,
-                                      const struct lgtd_lifx_packet_header *hdr,
-                                      const struct lgtd_lifx_packet_pan_gateway *pkt)
- {
--    (void)pkt;
--
-     assert(gw && hdr && pkt);
- 
-     lgtd_debug(
--        "SET_PAN_GATEWAY <-- [%s]:%hu - %s site=%s",
--        gw->ip_addr, gw->port,
--        lgtd_addrtoa(hdr->target.device_addr),
--        lgtd_addrtoa(hdr->site)
-+        "SET_PAN_GATEWAY <-- [%s]:%hu - %s site=%s, service_type=%d",
-+        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        lgtd_addrtoa(hdr->site), pkt->service_type
-     );
- }
- 
-@@ -485,16 +572,44 @@
- }
- 
- int
-+lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *gw,
-+                             const struct lgtd_lifx_tag *tag)
-+{
-+    assert(gw);
-+    assert(tag);
-+
-+    int tag_id;
-+    LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, gw->tag_ids) {
-+        if (gw->tags[tag_id] == tag) {
-+            return tag_id;
-+        }
-+    }
-+
-+    return -1;
-+}
-+
-+int
- lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw,
-                                   int tag_id,
-                                   const char *tag_label)
- {
-     assert(gw);
-     assert(tag_label);
--    // allocating a new tag_id (tag_id == -1) isn't supported yet:
--    assert(tag_id >= 0);
-+    assert(tag_id >= -1);
-     assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS);
- 
-+    if (tag_id == -1) {
-+        tag_id = lgtd_lifx_wire_bitscan64_forward(~gw->tag_ids);
-+        if (tag_id == -1) {
-+            lgtd_warnx(
-+                "no tag_id left for new tag [%s] on gw [%s]:%hu (site %s)",
-+                tag_label, gw->ip_addr, gw->port,
-+                lgtd_addrtoa(gw->site.as_array)
-+            );
-+            return -1;
-+        }
-+    }
-+
-     if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) {
-         struct lgtd_lifx_tag *tag;
-         tag = lgtd_lifx_tagging_incref(tag_label, gw, tag_id);
-@@ -545,9 +660,9 @@
-     assert(gw && hdr && pkt);
- 
-     lgtd_debug(
--        "SET_TAG_LABELS <-- [%s]:%hu - %s label=%s, tags=%jx",
-+        "SET_TAG_LABELS <-- [%s]:%hu - %s label=%.*s, tags=%jx",
-         gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
--        pkt->label, (uintmax_t)pkt->tags
-+        LGTD_LIFX_LABEL_SIZE, pkt->label, (uintmax_t)pkt->tags
-     );
- 
-     int tag_id;
-@@ -559,3 +674,38 @@
-         }
-     }
- }
-+
-+void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw,
-+                                   const struct lgtd_lifx_packet_header *hdr,
-+                                   const struct lgtd_lifx_packet_tags *pkt)
-+{
-+    assert(gw && hdr && pkt);
-+
-+    lgtd_debug(
-+        "SET_TAGS <-- [%s]:%hu - %s tags=%#jx",
-+        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        (uintmax_t)pkt->tags
-+    );
-+
-+    struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
-+        gw, hdr->target.device_addr
-+    );
-+    if (!b) {
-+        return;
-+    }
-+
-+    int tag_id;
-+    LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, pkt->tags) {
-+        if (!(gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))) {
-+            lgtd_warnx(
-+                "trying to set unknown tag_id %d (%#jx) "
-+                "on bulb %s (%.*s), gw [%s]:%hu (site %s)",
-+                tag_id, LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id),
-+                lgtd_addrtoa(b->addr), LGTD_LIFX_LABEL_SIZE, b->state.label,
-+                gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array)
-+            );
-+        }
-+    }
-+
-+    lgtd_lifx_bulb_set_tags(b, pkt->tags);
-+}
-diff --git a/lifx/gateway.h b/lifx/gateway.h
---- a/lifx/gateway.h
-+++ b/lifx/gateway.h
-@@ -51,6 +51,7 @@
-     }                               site;
-     uint64_t                        tag_ids;
-     struct lgtd_lifx_tag            *tags[LGTD_LIFX_GATEWAY_MAX_TAGS];
-+    uint8_t                         tag_refcounts[LGTD_LIFX_GATEWAY_MAX_TAGS];
-     evutil_socket_t                 socket;
-     // Those three timers let us measure the latency of the gateway. If we
-     // aren't the only client on the network then this won't be accurate since
-@@ -84,6 +85,7 @@
- 
- void lgtd_lifx_gateway_close(struct lgtd_lifx_gateway *);
- void lgtd_lifx_gateway_close_all(void);
-+void lgtd_lifx_gateway_remove_and_close_bulb(struct lgtd_lifx_gateway *, struct lgtd_lifx_bulb *);
- 
- void lgtd_lifx_gateway_force_refresh(struct lgtd_lifx_gateway *);
- 
-@@ -92,7 +94,14 @@
-                                       enum lgtd_lifx_packet_type,
-                                       const void *,
-                                       int);
-+// This could be on router but it's LIFX specific so I'd rather keep it here:
-+bool lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *,
-+                                    enum lgtd_lifx_packet_type,
-+                                    const void *);
- 
-+void lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *, uint64_t, uint64_t);
-+
-+int lgtd_lifx_gateway_get_tag_id(const struct lgtd_lifx_gateway *, const struct lgtd_lifx_tag *);
- int lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *, int, const char *);
- void lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *, int);
- 
-@@ -108,3 +117,6 @@
- void lgtd_lifx_gateway_handle_tag_labels(struct lgtd_lifx_gateway *,
-                                          const struct lgtd_lifx_packet_header *,
-                                          const struct lgtd_lifx_packet_tag_labels *);
-+void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *,
-+                                   const struct lgtd_lifx_packet_header *,
-+                                   const struct lgtd_lifx_packet_tags *);
-diff --git a/lifx/tagging.c b/lifx/tagging.c
---- a/lifx/tagging.c
-+++ b/lifx/tagging.c
-@@ -66,6 +66,32 @@
- }
- 
- struct lgtd_lifx_tag *
-+lgtd_lifx_tagging_allocate_tag(const char *tag_label)
-+{
-+    assert(tag_label);
-+    assert(strlen(tag_label) < LGTD_LIFX_LABEL_SIZE);
-+
-+    struct lgtd_lifx_tag *tag = calloc(1, sizeof(*tag));
-+    if (!tag) {
-+        return NULL;
-+    }
-+
-+    strncpy(tag->label, tag_label, sizeof(tag->label) - 1);
-+    LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link);
-+    return tag;
-+}
-+
-+void
-+lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *tag)
-+{
-+    assert(tag);
-+    assert(LIST_EMPTY(&tag->sites));
-+
-+    LIST_REMOVE(tag, link);
-+    free(tag);
-+}
-+
-+struct lgtd_lifx_tag *
- lgtd_lifx_tagging_incref(const char *tag_label,
-                          struct lgtd_lifx_gateway *gw,
-                          int tag_id)
-@@ -77,12 +103,10 @@
-     bool dealloc_tag = false;
-     struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label);
-     if (!tag) {
--        tag = calloc(1, sizeof(*tag));
-+        tag = lgtd_lifx_tagging_allocate_tag(tag_label);
-         if (!tag) {
-             return NULL;
-         }
--        strncpy(tag->label, tag_label, sizeof(tag->label) - 1);
--        LIST_INSERT_HEAD(&lgtd_lifx_tags, tag, link);
-         dealloc_tag = true;
-     }
- 
-@@ -91,8 +115,7 @@
-         site = calloc(1, sizeof(*site));
-         if (!site) {
-             if (dealloc_tag) {
--                LIST_REMOVE(tag, link);
--                free(tag);
-+                lgtd_lifx_tagging_deallocate_tag(tag);
-             }
-             errno = ENOMEM;
-             return NULL;
-@@ -100,9 +123,10 @@
-         if (dealloc_tag) {
-             lgtd_info("discovered tag [%s]", tag_label);
-         }
--        lgtd_debug(
--            "tag [%s] added to gw [%s]:%hu (site %s)",
--            tag_label, gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array)
-+        lgtd_info(
-+            "tag [%s] added to gw [%s]:%hu (site %s) with tag_id %d",
-+            tag_label, gw->ip_addr, gw->port,
-+            lgtd_addrtoa(gw->site.as_array), tag_id
-         );
-         site->gw = gw;
-         site->tag_id = tag_id;
-@@ -132,8 +156,7 @@
-         free(site);
-     }
-     if (LIST_EMPTY(&tag->sites)) {
--        LIST_REMOVE(tag, link);
-         lgtd_info("forgetting unused tag [%s]", tag->label);
--        free(tag);
-+        lgtd_lifx_tagging_deallocate_tag(tag);
-     }
- }
-diff --git a/lifx/tagging.h b/lifx/tagging.h
---- a/lifx/tagging.h
-+++ b/lifx/tagging.h
-@@ -39,3 +39,6 @@
- void lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *, struct lgtd_lifx_gateway *);
- 
- struct lgtd_lifx_tag *lgtd_lifx_tagging_find_tag(const char *);
-+struct lgtd_lifx_tag *lgtd_lifx_tagging_allocate_tag(const char *);
-+
-+void lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *);
-diff --git a/lifx/timer.c b/lifx/timer.c
---- a/lifx/timer.c
-+++ b/lifx/timer.c
-@@ -95,7 +95,7 @@
-                 "closing bulb \"%.*s\" that hasn't been updated for %dms",
-                 LGTD_LIFX_LABEL_SIZE, bulb->state.label, light_state_lag
-             );
--            lgtd_lifx_bulb_close(bulb);
-+            lgtd_lifx_gateway_remove_and_close_bulb(bulb->gw, bulb);
-             start_discovery = true;
-             continue;
-         }
-diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -92,6 +92,7 @@
-     .handle = lgtd_lifx_wire_null_packet_handler
- 
-     static struct lgtd_lifx_packet_infos packet_table[] = {
-+        // Gateway packets:
-         {
-             REQUEST_ONLY,
-             NO_PAYLOAD,
-@@ -108,6 +109,43 @@
-         },
-         {
-             REQUEST_ONLY,
-+            .name = "SET_TAG_LABELS",
-+            .type = LGTD_LIFX_SET_TAG_LABELS,
-+            .size = sizeof(struct lgtd_lifx_packet_tag_labels),
-+            .encode = ENCODER(lgtd_lifx_wire_encode_tag_labels)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            .name = "GET_TAG_LABELS",
-+            .type = LGTD_LIFX_GET_TAG_LABELS,
-+            .size = sizeof(struct lgtd_lifx_packet_tags),
-+            .encode = ENCODER(lgtd_lifx_wire_encode_tags)
-+        },
-+        {
-+            RESPONSE_ONLY,
-+            .name = "TAG_LABELS",
-+            .type = LGTD_LIFX_TAG_LABELS,
-+            .size = sizeof(struct lgtd_lifx_packet_tag_labels),
-+            .decode = DECODER(lgtd_lifx_wire_decode_tag_labels),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_tag_labels)
-+        },
-+        // Bulb packets:
-+        {
-+            REQUEST_ONLY,
-+            .name = "SET_LIGHT_COLOR",
-+            .type = LGTD_LIFX_SET_LIGHT_COLOR,
-+            .size = sizeof(struct lgtd_lifx_packet_light_color),
-+            .encode = ENCODER(lgtd_lifx_wire_encode_light_color)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            .name = "SET_WAVEFORM",
-+            .type = LGTD_LIFX_SET_WAVEFORM,
-+            .size = sizeof(struct lgtd_lifx_packet_waveform),
-+            .encode = ENCODER(lgtd_lifx_wire_encode_waveform)
-+        },
-+        {
-+            REQUEST_ONLY,
-             NO_PAYLOAD,
-             .name = "GET_LIGHT_STATUS",
-             .type = LGTD_LIFX_GET_LIGHT_STATE
-@@ -128,6 +166,7 @@
-             .type = LGTD_LIFX_SET_POWER_STATE,
-         },
-         {
-+            RESPONSE_ONLY,
-             .name = "POWER_STATE",
-             .type = LGTD_LIFX_POWER_STATE,
-             .size = sizeof(struct lgtd_lifx_packet_power_state),
-@@ -136,32 +175,18 @@
-         },
-         {
-             REQUEST_ONLY,
--            .name = "SET_LIGHT_COLOR",
--            .type = LGTD_LIFX_SET_LIGHT_COLOR,
--            .size = sizeof(struct lgtd_lifx_packet_light_color),
--            .encode = ENCODER(lgtd_lifx_wire_encode_light_color)
--        },
--        {
--            REQUEST_ONLY,
--            .name = "SET_WAVEFORM",
--            .type = LGTD_LIFX_SET_WAVEFORM,
--            .size = sizeof(struct lgtd_lifx_packet_waveform),
--            .encode = ENCODER(lgtd_lifx_wire_encode_waveform)
--        },
--        {
--            REQUEST_ONLY,
--            .name = "GET_TAG_LABELS",
--            .type = LGTD_LIFX_GET_TAG_LABELS,
--            .size = sizeof(struct lgtd_lifx_packet_get_tag_labels),
--            .encode = lgtd_lifx_wire_null_packet_encoder_decoder
-+            .name = "SET_TAGS",
-+            .type = LGTD_LIFX_SET_TAGS,
-+            .size = sizeof(struct lgtd_lifx_packet_tags),
-+            .encode = ENCODER(lgtd_lifx_wire_encode_tags)
-         },
-         {
-             RESPONSE_ONLY,
--            .name = "TAG_LABELS",
--            .type = LGTD_LIFX_TAG_LABELS,
--            .size = sizeof(struct lgtd_lifx_packet_tag_labels),
--            .decode = DECODER(lgtd_lifx_wire_decode_tag_labels),
--            .handle = HANDLER(lgtd_lifx_gateway_handle_tag_labels)
-+            .name = "TAGS",
-+            .type = LGTD_LIFX_TAGS,
-+            .size = sizeof(struct lgtd_lifx_packet_tags),
-+            .decode = DECODER(lgtd_lifx_wire_decode_tags),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_tags)
-         }
-     };
- 
-@@ -356,6 +381,14 @@
- }
- 
- void
-+lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt)
-+{
-+    assert(pkt);
-+
-+    pkt->tags = htole64(pkt->tags);
-+}
-+
-+void
- lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *pkt)
- {
-     assert(pkt);
-@@ -363,3 +396,19 @@
-     pkt->label[sizeof(pkt->label) - 1] = '\0';
-     pkt->tags = le64toh(pkt->tags);
- }
-+
-+void
-+lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *pkt)
-+{
-+    assert(pkt);
-+
-+    pkt->tags = htole64(pkt->tags);
-+}
-+
-+void
-+lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *pkt)
-+{
-+    assert(pkt);
-+
-+    pkt->tags = le64toh(pkt->tags);
-+}
-diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
---- a/lifx/wire_proto.h
-+++ b/lifx/wire_proto.h
-@@ -238,7 +238,7 @@
- };
- 
- enum { LGTD_LIFX_ALL_TAGS = ~0 };
--struct lgtd_lifx_packet_get_tag_labels {
-+struct lgtd_lifx_packet_tags {
-     uint64le_t  tags;
- };
- 
-@@ -350,4 +350,7 @@
- void lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *);
- void lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *);
- 
-+void lgtd_lifx_wire_encode_tags(struct lgtd_lifx_packet_tags *);
-+void lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *);
-+void lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *);
- void lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *);
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag.c
-@@ -0,0 +1,65 @@
-+#include "jsonrpc.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_gateway.h"
-+
-+#define MOCKED_LGTD_TAG
-+#include "test_jsonrpc_utils.h"
-+
-+static bool tag_called = false;
-+
-+void
-+lgtd_proto_tag(struct lgtd_client *client,
-+               const struct lgtd_proto_target_list *targets,
-+               const char *tag)
-+{
-+    if (!client) {
-+        errx(1, "missing client!");
-+    }
-+
-+    if (strcmp(SLIST_FIRST(targets)->target, "*")) {
-+        errx(
-+            1, "Invalid target [%s] (expected=[*])",
-+            SLIST_FIRST(targets)->target
-+        );
-+    }
-+
-+    if (strcmp(tag, "suspensions")) {
-+        errx(1, "Invalid tag [%s] (expected=[suspensions])", tag);
-+    }
-+
-+    tag_called = true;
-+}
-+
-+int
-+main(void)
-+{
-+    jsmntok_t tokens[32];
-+    const char json[] = ("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"tag\","
-+        "\"params\": {\"target\": \"*\", \"tag\": \"suspensions\"},"
-+        "\"id\": \"42\""
-+    "}");
-+    int parsed = parse_json(
-+        tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)
-+    );
-+
-+    bool ok;
-+    struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER;
-+    struct lgtd_client client = {
-+        .io = NULL, .current_request = &req, .json = json
-+    };
-+    ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-+    if (!ok) {
-+        errx(1, "can't parse request");
-+    }
-+
-+    lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_tag);
-+
-+    if (!tag_called) {
-+        errx(1, "lgtd_proto_tag wasn't called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_tag_missing_params.c
-@@ -0,0 +1,53 @@
-+#include "jsonrpc.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_gateway.h"
-+
-+#define MOCKED_LGTD_TAG
-+#include "test_jsonrpc_utils.h"
-+
-+static bool tag_called = false;
-+
-+void
-+lgtd_proto_tag(struct lgtd_client *client,
-+               const struct lgtd_proto_target_list *targets,
-+               const char *tag)
-+{
-+    (void)client;
-+    (void)targets;
-+    (void)tag;
-+    tag_called = true;
-+}
-+
-+int
-+main(void)
-+{
-+    jsmntok_t tokens[32];
-+    const char json[] = ("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"tag\","
-+        "\"params\": {\"tag\": \"suspensions\"},"
-+        "\"id\": \"42\""
-+    "}");
-+    int parsed = parse_json(
-+        tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)
-+    );
-+
-+    bool ok;
-+    struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER;
-+    struct lgtd_client client = {
-+        .io = NULL, .current_request = &req, .json = json
-+    };
-+    ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-+    if (!ok) {
-+        errx(1, "can't parse request");
-+    }
-+
-+    lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_tag);
-+
-+    if (tag_called) {
-+        errx(1, "lgtd_proto_tag was called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag.c
-@@ -0,0 +1,65 @@
-+#include "jsonrpc.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_gateway.h"
-+
-+#define MOCKED_LGTD_UNTAG
-+#include "test_jsonrpc_utils.h"
-+
-+static bool untag_called = false;
-+
-+void
-+lgtd_proto_untag(struct lgtd_client *client,
-+                 const struct lgtd_proto_target_list *targets,
-+                 const char *tag)
-+{
-+    if (!client) {
-+        errx(1, "missing client!");
-+    }
-+
-+    if (strcmp(SLIST_FIRST(targets)->target, "#suspensions")) {
-+        errx(
-+            1, "Invalid target [%s] (expected=[#suspensions])",
-+            SLIST_FIRST(targets)->target
-+        );
-+    }
-+
-+    if (strcmp(tag, "suspensions")) {
-+        errx(1, "Invalid tag [%s] (expected=[suspensions])", tag);
-+    }
-+
-+    untag_called = true;
-+}
-+
-+int
-+main(void)
-+{
-+    jsmntok_t tokens[32];
-+    const char json[] = ("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"tag\","
-+        "\"params\": [[\"#suspensions\"], \"suspensions\"],"
-+        "\"id\": \"42\""
-+    "}");
-+    int parsed = parse_json(
-+        tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)
-+    );
-+
-+    bool ok;
-+    struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER;
-+    struct lgtd_client client = {
-+        .io = NULL, .current_request = &req, .json = json
-+    };
-+    ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-+    if (!ok) {
-+        errx(1, "can't parse request");
-+    }
-+
-+    lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_untag);
-+
-+    if (!untag_called) {
-+        errx(1, "lgtd_proto_tag wasn't called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_untag_invalid_params.c
-@@ -0,0 +1,53 @@
-+#include "jsonrpc.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_gateway.h"
-+
-+#define MOCKED_LGTD_UNTAG
-+#include "test_jsonrpc_utils.h"
-+
-+static bool untag_called = false;
-+
-+void
-+lgtd_proto_untag(struct lgtd_client *client,
-+                 const struct lgtd_proto_target_list *targets,
-+                 const char *tag)
-+{
-+    (void)client;
-+    (void)targets;
-+    (void)tag;
-+    untag_called = true;
-+}
-+
-+int
-+main(void)
-+{
-+    jsmntok_t tokens[32];
-+    const char json[] = ("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"tag\","
-+        "\"params\": [[\"#suspensions\"], [\"suspensions\"]],"
-+        "\"id\": \"42\""
-+    "}");
-+    int parsed = parse_json(
-+        tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)
-+    );
-+
-+    bool ok;
-+    struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER;
-+    struct lgtd_client client = {
-+        .io = NULL, .current_request = &req, .json = json
-+    };
-+    ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-+    if (!ok) {
-+        errx(1, "can't parse request");
-+    }
-+
-+    lgtd_jsonrpc_check_and_call_proto_tag_or_untag(&client, lgtd_proto_untag);
-+
-+    if (untag_called) {
-+        errx(1, "lgtd_proto_tag was called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/jsonrpc/test_jsonrpc_utils.h b/tests/core/jsonrpc/test_jsonrpc_utils.h
---- a/tests/core/jsonrpc/test_jsonrpc_utils.h
-+++ b/tests/core/jsonrpc/test_jsonrpc_utils.h
-@@ -97,3 +97,27 @@
-     (void)targets;
- }
- #endif
-+
-+#ifndef MOCKED_LGTD_TAG
-+void
-+lgtd_proto_tag(struct lgtd_client *client,
-+               const struct lgtd_proto_target_list *targets,
-+               const char *tag_label)
-+{
-+    (void)client;
-+    (void)targets;
-+    (void)tag_label;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_UNTAG
-+void
-+lgtd_proto_untag(struct lgtd_client *client,
-+                 const struct lgtd_proto_target_list *targets,
-+                 const char *tag_label)
-+{
-+    (void)client;
-+    (void)targets;
-+    (void)tag_label;
-+}
-+#endif
-diff --git a/tests/core/proto/test_proto_tag_create.c b/tests/core/proto/test_proto_tag_create.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/proto/test_proto_tag_create.c
-@@ -0,0 +1,253 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_daemon.h"
-+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
-+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID
-+#include "mock_gateway.h"
-+#include "tests_utils.h"
-+
-+#define MOCKED_ROUTER_TARGETS_TO_DEVICES
-+#define MOCKED_ROUTER_SEND_TO_DEVICE
-+#define MOCKED_ROUTER_DEVICE_LIST_FREE
-+#include "tests_proto_utils.h"
-+
-+#define FAKE_TARGET_LIST (void *)0x2a
-+
-+static struct lgtd_router_device_list devices =
-+    SLIST_HEAD_INITIALIZER(&devices);
-+static struct lgtd_router_device_list device_1_only =
-+    SLIST_HEAD_INITIALIZER(&device_1_only);
-+
-+static bool send_to_device_called = false;
-+
-+void
-+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb,
-+                           enum lgtd_lifx_packet_type pkt_type,
-+                           void *pkt)
-+{
-+    if (!bulb) {
-+        errx(1, "lgtd_router_send_to_device must be called with a bulb");
-+    }
-+
-+    uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 1, 2, 3, 4, 5 };
-+    if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) {
-+        errx(
-+            1, "got bulb with addr %s (expected %s)",
-+            lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr)
-+        );
-+    }
-+
-+    if (pkt_type != LGTD_LIFX_SET_TAGS) {
-+        errx(
-+            1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS
-+        );
-+    }
-+
-+    if (!pkt) {
-+        errx(1, "missing SET_TAGS payload");
-+    }
-+
-+    const struct lgtd_lifx_packet_tags *pkt_tags = pkt;
-+    uint64_t tags = le64toh(pkt_tags->tags);
-+    if (tags != 0x1) {
-+        errx(
-+            1, "invalid SET_TAGS payload=%#jx (expected %#x)",
-+            (uintmax_t)tags, 0x1
-+        );
-+    }
-+
-+    send_to_device_called = true;
-+}
-+
-+static bool gateway_send_to_site_called = false;
-+
-+bool
-+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-+                               enum lgtd_lifx_packet_type pkt_type,
-+                               const void *pkt)
-+{
-+    if (!gw) {
-+        errx(1, "missing gateway");
-+    }
-+
-+    if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) {
-+        errx(
-+            1, "got packet type %#x (expected %#x)",
-+            pkt_type, LGTD_LIFX_SET_TAG_LABELS
-+        );
-+    }
-+
-+    const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt;
-+    uint64_t tags = le64toh(pkt_tag_labels->tags);
-+    if (tags != 0x1) {
-+        errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x1);
-+    }
-+
-+    if (strcmp(pkt_tag_labels->label, "dub")) {
-+        errx(1, "got label %s (expected dub)", pkt_tag_labels->label);
-+    }
-+
-+    gateway_send_to_site_called = true;
-+
-+    return true;
-+}
-+
-+static bool gateway_allocate_tag_id_called = false;
-+
-+int
-+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw,
-+                                  int tag_id,
-+                                  const char *tag_label)
-+{
-+    if (gateway_allocate_tag_id_called) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "should have been called once only"
-+        );
-+    }
-+
-+    if (tag_id != -1) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "tag_id %d (expected -1)", tag_id
-+        );
-+    }
-+
-+    if (!gw) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "must be called with gateway"
-+        );
-+    }
-+
-+    if (!tag_label) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "must be called with a tag_label"
-+        );
-+    }
-+
-+    tag_id = 0;
-+
-+    struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label);
-+    if (!tag) {
-+        errx(1, "tag %s wasn't found", tag_label);
-+    }
-+    lgtd_tests_add_tag_to_gw(tag, gw, tag_id);
-+
-+    gateway_allocate_tag_id_called = true;
-+
-+    return tag_id;
-+}
-+
-+static bool device_list_free_called = false;
-+
-+void
-+lgtd_router_device_list_free(struct lgtd_router_device_list *devices)
-+{
-+    if (!devices) {
-+        lgtd_errx(1, "the device list must be passed");
-+    }
-+
-+    device_list_free_called = true;
-+}
-+
-+struct lgtd_router_device_list *
-+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets)
-+{
-+    if (targets != FAKE_TARGET_LIST) {
-+        lgtd_errx(1, "unexpected targets list");
-+    }
-+
-+    return &device_1_only;
-+}
-+
-+static void
-+setup_devices(void)
-+{
-+    static struct lgtd_lifx_gateway gw_bulb_1 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs)
-+    };
-+    static struct lgtd_lifx_bulb bulb_1 = {
-+        .addr = { 1, 2, 3, 4, 5 },
-+        .state = {
-+            .hue = 0xaaaa,
-+            .saturation = 0xffff,
-+            .brightness = 0xbbbb,
-+            .kelvin = 3600,
-+            .label = "wave",
-+            .power = LGTD_LIFX_POWER_ON,
-+            .tags = 0
-+        },
-+        .gw = &gw_bulb_1
-+    };
-+    static struct lgtd_router_device device_1 = { .device = &bulb_1 };
-+    SLIST_INSERT_HEAD(&devices, &device_1, link);
-+    SLIST_INSERT_HEAD(&device_1_only, &device_1, link);
-+
-+    struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor");
-+    struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b");
-+    struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~");
-+    static struct lgtd_lifx_gateway gw_bulb_2 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs),
-+        .tag_ids = 0x7
-+    };
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2);
-+    static struct lgtd_lifx_bulb bulb_2 = {
-+        .addr = { 5, 4, 3, 2, 1 },
-+        .state = {
-+            .hue = 0x0000,
-+            .saturation = 0x0000,
-+            .brightness = 0xffff,
-+            .kelvin = 4000,
-+            .label = "",
-+            .power = LGTD_LIFX_POWER_OFF,
-+            .tags = 0x3
-+        },
-+        .gw = &gw_bulb_2
-+    };
-+    static struct lgtd_router_device device_2 = { .device = &bulb_2 };
-+    SLIST_INSERT_HEAD(&devices, &device_2, link);
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-+
-+    setup_devices();
-+
-+    lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub");
-+
-+    const char expected[] = "true";
-+    if (client_write_buf_idx != sizeof(expected) - 1) {
-+        lgtd_errx(
-+            1,
-+            "%d bytes written, expected %lu "
-+            "(got %.*s instead of %s)",
-+            client_write_buf_idx, sizeof(expected) - 1UL,
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+    if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) {
-+        lgtd_errx(
-+            1, "got %.*s instead of %s",
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+
-+    if (!gateway_send_to_site_called) {
-+        lgtd_errx(1, "SET_TAG_LABELS wasn't sent");
-+    }
-+    if (!device_list_free_called) {
-+        lgtd_errx(1, "the list of devices hasn't been freed");
-+    }
-+    if (!send_to_device_called) {
-+        lgtd_errx(1, "SET_TAGS wasn't send to any device");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
-@@ -0,0 +1,209 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_daemon.h"
-+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
-+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID
-+#include "mock_gateway.h"
-+#include "tests_utils.h"
-+
-+#define MOCKED_CLIENT_SEND_ERROR
-+#define MOCKED_ROUTER_TARGETS_TO_DEVICES
-+#define MOCKED_ROUTER_SEND_TO_DEVICE
-+#define MOCKED_ROUTER_DEVICE_LIST_FREE
-+#include "tests_proto_utils.h"
-+
-+#define FAKE_TARGET_LIST (void *)0x2a
-+
-+static struct lgtd_router_device_list devices =
-+    SLIST_HEAD_INITIALIZER(&devices);
-+static struct lgtd_router_device_list device_1_only =
-+    SLIST_HEAD_INITIALIZER(&device_1_only);
-+
-+static bool client_send_error_called = false;
-+
-+void
-+lgtd_client_send_error(struct lgtd_client *client,
-+                       enum lgtd_client_error_code error,
-+                       const char *msg)
-+{
-+    if (!client) {
-+        errx(1, "client_send_error called without a client");
-+    }
-+
-+    if (!error) {
-+        errx(1, "client_send_error called without an error code");
-+    }
-+
-+    if (!msg) {
-+        errx(1, "client_send_error called without an error message");
-+    }
-+
-+    client_send_error_called = true;
-+}
-+
-+static bool send_to_device_called = false;
-+
-+void
-+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb,
-+                           enum lgtd_lifx_packet_type pkt_type,
-+                           void *pkt)
-+{
-+    (void)bulb;
-+    (void)pkt_type;
-+    (void)pkt;
-+
-+    send_to_device_called = true;
-+}
-+
-+static bool gateway_send_to_site_called = false;
-+
-+bool
-+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-+                               enum lgtd_lifx_packet_type pkt_type,
-+                               const void *pkt)
-+{
-+    (void)gw;
-+    (void)pkt_type;
-+    (void)pkt;
-+
-+    gateway_send_to_site_called = true;
-+
-+    return true;
-+}
-+
-+static bool gateway_allocate_tag_id_called = false;
-+
-+int
-+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw,
-+                                  int tag_id,
-+                                  const char *tag_label)
-+{
-+    if (gateway_allocate_tag_id_called) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "should have been called once only"
-+        );
-+    }
-+
-+    if (tag_id != -1) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "tag_id %d (expected -1)", tag_id
-+        );
-+    }
-+
-+    if (!gw) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "must be called with gateway"
-+        );
-+    }
-+
-+    if (!tag_label) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "must be called with a tag_label"
-+        );
-+    }
-+
-+    return -1;  // no more tag id available
-+}
-+
-+static bool device_list_free_called = false;
-+
-+void
-+lgtd_router_device_list_free(struct lgtd_router_device_list *devices)
-+{
-+    if (!devices) {
-+        lgtd_errx(1, "the device list must be passed");
-+    }
-+
-+    device_list_free_called = true;
-+}
-+
-+struct lgtd_router_device_list *
-+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets)
-+{
-+    if (targets != FAKE_TARGET_LIST) {
-+        lgtd_errx(1, "unexpected targets list");
-+    }
-+
-+    return &device_1_only;
-+}
-+
-+static void
-+setup_devices(void)
-+{
-+    static struct lgtd_lifx_gateway gw_bulb_1 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs)
-+    };
-+    static struct lgtd_lifx_bulb bulb_1 = {
-+        .addr = { 1, 2, 3, 4, 5 },
-+        .state = {
-+            .hue = 0xaaaa,
-+            .saturation = 0xffff,
-+            .brightness = 0xbbbb,
-+            .kelvin = 3600,
-+            .label = "wave",
-+            .power = LGTD_LIFX_POWER_ON,
-+            .tags = 0
-+        },
-+        .gw = &gw_bulb_1
-+    };
-+    static struct lgtd_router_device device_1 = { .device = &bulb_1 };
-+    SLIST_INSERT_HEAD(&devices, &device_1, link);
-+    SLIST_INSERT_HEAD(&device_1_only, &device_1, link);
-+
-+    struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor");
-+    struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b");
-+    struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~");
-+    static struct lgtd_lifx_gateway gw_bulb_2 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs),
-+        .tag_ids = 0x7
-+    };
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2);
-+    static struct lgtd_lifx_bulb bulb_2 = {
-+        .addr = { 5, 4, 3, 2, 1 },
-+        .state = {
-+            .hue = 0x0000,
-+            .saturation = 0x0000,
-+            .brightness = 0xffff,
-+            .kelvin = 4000,
-+            .label = "",
-+            .power = LGTD_LIFX_POWER_OFF,
-+            .tags = 0x3
-+        },
-+        .gw = &gw_bulb_2
-+    };
-+    static struct lgtd_router_device device_2 = { .device = &bulb_2 };
-+    SLIST_INSERT_HEAD(&devices, &device_2, link);
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-+
-+    setup_devices();
-+
-+    lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub");
-+
-+
-+    if (gateway_send_to_site_called) {
-+        lgtd_errx(1, "SET_TAG_LABELS shouldn't have been sent");
-+    }
-+    if (!device_list_free_called) {
-+        lgtd_errx(1, "the list of devices hasn't been freed");
-+    }
-+    if (send_to_device_called) {
-+        lgtd_errx(1, "SET_TAGS shouldn't have been to any device");
-+    }
-+    if (!client_send_error_called) {
-+        lgtd_errx(1, "client_send_error should have been called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/proto/test_proto_tag_update.c b/tests/core/proto/test_proto_tag_update.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/proto/test_proto_tag_update.c
-@@ -0,0 +1,283 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_daemon.h"
-+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
-+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID
-+#include "mock_gateway.h"
-+#include "tests_utils.h"
-+
-+#define MOCKED_ROUTER_TARGETS_TO_DEVICES
-+#define MOCKED_ROUTER_SEND_TO_DEVICE
-+#define MOCKED_ROUTER_DEVICE_LIST_FREE
-+#include "tests_proto_utils.h"
-+
-+#define FAKE_TARGET_LIST (void *)0x2a
-+
-+static struct lgtd_router_device_list devices =
-+    SLIST_HEAD_INITIALIZER(&devices);
-+
-+static bool send_to_device_called = false;
-+
-+void
-+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb,
-+                           enum lgtd_lifx_packet_type pkt_type,
-+                           void *pkt)
-+{
-+    if (send_to_device_called) {
-+        errx(1, "lgtd_router_send_to_device should have been called once only");
-+    }
-+
-+    if (!bulb) {
-+        errx(1, "lgtd_router_send_to_device must be called with a bulb");
-+    }
-+
-+    uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 };
-+    if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) {
-+        errx(
-+            1, "got bulb with addr %s (expected %s)",
-+            lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr)
-+        );
-+    }
-+
-+    if (pkt_type != LGTD_LIFX_SET_TAGS) {
-+        errx(
-+            1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS
-+        );
-+    }
-+
-+    if (!pkt) {
-+        errx(1, "missing SET_TAGS payload");
-+    }
-+
-+    const struct lgtd_lifx_packet_tags *pkt_tags = pkt;
-+    uint64_t tags = le64toh(pkt_tags->tags);
-+
-+    if (tags != 0x7) {
-+        errx(
-+            1, "invalid SET_TAGS payload=%#jx (expected %#x)",
-+            (uintmax_t)tags, 0x7
-+        );
-+    }
-+
-+    send_to_device_called = true;
-+}
-+
-+static bool gateway_send_to_site_called_for_gw_1 = false;
-+static bool gateway_send_to_site_called_for_gw_2 = false;
-+
-+bool
-+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-+                               enum lgtd_lifx_packet_type pkt_type,
-+                               const void *pkt)
-+{
-+    if (!gw) {
-+        errx(1, "missing gateway");
-+    }
-+
-+    if (pkt_type != LGTD_LIFX_SET_TAG_LABELS) {
-+        errx(
-+            1, "got packet type %#x (expected %#x)",
-+            pkt_type, LGTD_LIFX_SET_TAG_LABELS
-+        );
-+    }
-+
-+    const struct lgtd_lifx_packet_tag_labels *pkt_tag_labels = pkt;
-+    uint64_t tags = le64toh(pkt_tag_labels->tags);
-+
-+    if (strcmp(pkt_tag_labels->label, "dub")) {
-+        errx(1, "got label %s (expected dub)", pkt_tag_labels->label);
-+    }
-+
-+    if (gw->site.as_integer == 42) {
-+        if (tags != 0x1) {
-+            errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x1);
-+        }
-+        if (gateway_send_to_site_called_for_gw_1) {
-+            errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 1");
-+        }
-+        gateway_send_to_site_called_for_gw_1 = true;
-+    } else if (gw->site.as_integer == 44) {
-+        if (tags != 0x4) {
-+            errx(1, "got tags %#jx (expected %#x)", (uintmax_t)tags, 0x4);
-+        }
-+        if (gateway_send_to_site_called_for_gw_2) {
-+            errx(1, "LGTD_LIFX_SET_TAG_LABELS already called for gw 2");
-+        }
-+        gateway_send_to_site_called_for_gw_2 = true;
-+    } else {
-+        errx(1, "LGTD_LIFX_SET_TAG_LABELS received an invalid gateway");
-+    }
-+
-+    return true;
-+}
-+
-+static bool gateway_allocate_tag_id_called = false;
-+
-+int
-+lgtd_lifx_gateway_allocate_tag_id(struct lgtd_lifx_gateway *gw,
-+                                  int tag_id,
-+                                  const char *tag_label)
-+{
-+    if (gateway_allocate_tag_id_called) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "should have been called once only"
-+        );
-+    }
-+
-+    if (tag_id != -1) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "tag_id %d (expected -1)", tag_id
-+        );
-+    }
-+
-+    if (!gw) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "must be called with gateway"
-+        );
-+    }
-+
-+    if (!tag_label) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id "
-+            "must be called with a tag_label"
-+        );
-+    }
-+
-+    if (gw->site.as_integer != 44) {
-+        errx(
-+            1, "lgtd_lifx_gateway_allocate_tag_id got the wrong gateway "
-+            "%#jx (expected %d)", (uintmax_t)gw->site.as_integer, 44
-+        );
-+    }
-+
-+    tag_id = 2;
-+
-+    struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(tag_label);
-+    if (!tag) {
-+        errx(1, "tag %s wasn't found", tag_label);
-+    }
-+    lgtd_tests_add_tag_to_gw(tag, gw, tag_id);
-+
-+    gateway_allocate_tag_id_called = true;
-+
-+    return tag_id;
-+}
-+
-+static bool device_list_free_called = false;
-+
-+void
-+lgtd_router_device_list_free(struct lgtd_router_device_list *devices)
-+{
-+    if (!devices) {
-+        lgtd_errx(1, "the device list must be passed");
-+    }
-+
-+    device_list_free_called = true;
-+}
-+
-+struct lgtd_router_device_list *
-+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets)
-+{
-+    if (targets != FAKE_TARGET_LIST) {
-+        lgtd_errx(1, "unexpected targets list");
-+    }
-+
-+    return &devices;
-+}
-+
-+static void
-+setup_devices(void)
-+{
-+    static struct lgtd_lifx_gateway gw_bulb_1 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs),
-+        .site = { .as_integer = 42 }
-+    };
-+    static struct lgtd_lifx_bulb bulb_1 = {
-+        .addr = { 1, 2, 3, 4, 5 },
-+        .state = {
-+            .hue = 0xaaaa,
-+            .saturation = 0xffff,
-+            .brightness = 0xbbbb,
-+            .kelvin = 3600,
-+            .label = "wave",
-+            .power = LGTD_LIFX_POWER_ON,
-+            .tags = 1
-+        },
-+        .gw = &gw_bulb_1
-+    };
-+    static struct lgtd_router_device device_1 = { .device = &bulb_1 };
-+    SLIST_INSERT_HEAD(&devices, &device_1, link);
-+    struct lgtd_lifx_tag *gw_1_tag_1 = lgtd_tests_insert_mock_tag("dub");
-+    lgtd_tests_add_tag_to_gw(gw_1_tag_1, &gw_bulb_1, 0);
-+
-+    struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor");
-+    struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b");
-+    static struct lgtd_lifx_gateway gw_bulb_2 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs),
-+        .site = { .as_integer = 44 },
-+        .tag_ids = 0x3
-+    };
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1);
-+    static struct lgtd_lifx_bulb bulb_2 = {
-+        .addr = { 5, 4, 3, 2, 1 },
-+        .state = {
-+            .hue = 0x0000,
-+            .saturation = 0x0000,
-+            .brightness = 0xffff,
-+            .kelvin = 4000,
-+            .label = "",
-+            .power = LGTD_LIFX_POWER_OFF,
-+            .tags = 0x3
-+        },
-+        .gw = &gw_bulb_2
-+    };
-+    static struct lgtd_router_device device_2 = { .device = &bulb_2 };
-+    SLIST_INSERT_HEAD(&devices, &device_2, link);
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-+
-+    setup_devices();
-+
-+    lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub");
-+
-+    const char expected[] = "true";
-+    if (client_write_buf_idx != sizeof(expected) - 1) {
-+        lgtd_errx(
-+            1,
-+            "%d bytes written, expected %lu "
-+            "(got %.*s instead of %s)",
-+            client_write_buf_idx, sizeof(expected) - 1UL,
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+    if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) {
-+        lgtd_errx(
-+            1, "got %.*s instead of %s",
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+
-+    if (!gateway_send_to_site_called_for_gw_1) {
-+        lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 1");
-+    }
-+    if (!gateway_send_to_site_called_for_gw_2) {
-+        lgtd_errx(1, "SET_TAG_LABELS wasn't sent to gw 2");
-+    }
-+    if (!device_list_free_called) {
-+        lgtd_errx(1, "the list of devices hasn't been freed");
-+    }
-+    if (!send_to_device_called) {
-+        lgtd_errx(1, "SET_TAGS wasn't send to any device");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/proto/test_proto_untag.c b/tests/core/proto/test_proto_untag.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/proto/test_proto_untag.c
-@@ -0,0 +1,170 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_daemon.h"
-+#include "mock_gateway.h"
-+#include "tests_utils.h"
-+
-+#define MOCKED_ROUTER_TARGETS_TO_DEVICES
-+#define MOCKED_ROUTER_SEND_TO_DEVICE
-+#define MOCKED_ROUTER_DEVICE_LIST_FREE
-+#include "tests_proto_utils.h"
-+
-+static bool device_list_free_called = false;
-+
-+void
-+lgtd_router_device_list_free(struct lgtd_router_device_list *devices)
-+{
-+    if (device_list_free_called) {
-+        errx(1, "the device list should have been freed once");
-+    }
-+
-+    if (!devices) {
-+        errx(1, "the device list must be passed");
-+    }
-+
-+    device_list_free_called = true;
-+}
-+
-+static struct lgtd_lifx_tag *tag_vapor = NULL;
-+
-+struct lgtd_router_device_list *
-+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets)
-+{
-+    if (targets != (void *)0x2a) {
-+        lgtd_errx(1, "unexpected targets list");
-+    }
-+
-+    static struct lgtd_router_device_list devices =
-+        SLIST_HEAD_INITIALIZER(&devices);
-+
-+    static struct lgtd_lifx_gateway gw_bulb_1 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs)
-+    };
-+    static struct lgtd_lifx_bulb bulb_1 = {
-+        .addr = { 1, 2, 3, 4, 5 },
-+        .state = {
-+            .hue = 0xaaaa,
-+            .saturation = 0xffff,
-+            .brightness = 0xbbbb,
-+            .kelvin = 3600,
-+            .label = "wave",
-+            .power = LGTD_LIFX_POWER_ON,
-+            .tags = 0
-+        },
-+        .gw = &gw_bulb_1
-+    };
-+    static struct lgtd_router_device device_1 = { .device = &bulb_1 };
-+    SLIST_INSERT_HEAD(&devices, &device_1, link);
-+
-+    struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b");
-+    struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~");
-+    static struct lgtd_lifx_gateway gw_bulb_2 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs),
-+        .tag_ids = 0x7
-+    };
-+    lgtd_tests_add_tag_to_gw(tag_vapor, &gw_bulb_2, 0);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2);
-+    static struct lgtd_lifx_bulb bulb_2 = {
-+        .addr = { 5, 4, 3, 2, 1 },
-+        .state = {
-+            .hue = 0x0000,
-+            .saturation = 0x0000,
-+            .brightness = 0xffff,
-+            .kelvin = 4000,
-+            .label = "",
-+            .power = LGTD_LIFX_POWER_OFF,
-+            .tags = 0x3
-+        },
-+        .gw = &gw_bulb_2
-+    };
-+    static struct lgtd_router_device device_2 = { .device = &bulb_2 };
-+    SLIST_INSERT_HEAD(&devices, &device_2, link);
-+
-+    return &devices;
-+}
-+
-+static bool send_to_device_called = false;
-+
-+void
-+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb,
-+                           enum lgtd_lifx_packet_type pkt_type,
-+                           void *pkt)
-+{
-+    if (send_to_device_called) {
-+        errx(1, "lgtd_router_send_to_device should have been called once");
-+    }
-+
-+    if (!bulb) {
-+        errx(1, "lgtd_router_send_to_device must be called with a bulb");
-+    }
-+
-+    uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 };
-+    if (memcmp(bulb->addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) {
-+        errx(
-+            1, "got bulb with addr %s (expected %s)",
-+            lgtd_addrtoa(bulb->addr), lgtd_addrtoa(expected_addr)
-+        );
-+    }
-+
-+    if (pkt_type != LGTD_LIFX_SET_TAGS) {
-+        errx(
-+            1, "got packet type %d (expected %d)", pkt_type, LGTD_LIFX_SET_TAGS
-+        );
-+    }
-+
-+    if (!pkt) {
-+        errx(1, "missing SET_TAGS payload");
-+    }
-+
-+    struct lgtd_lifx_packet_tags *pkt_tags = pkt;
-+    if (pkt_tags->tags != 0x2) {
-+        errx(
-+            1, "invalid SET_TAGS payload=%#jx (expected %#x)",
-+            (uintmax_t)pkt_tags->tags, 0x2
-+        );
-+    }
-+
-+    send_to_device_called = true;
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-+
-+    struct lgtd_proto_target_list *targets = (void *)0x2a;
-+
-+    tag_vapor = lgtd_tests_insert_mock_tag("vapor");
-+
-+    lgtd_proto_untag(&client, targets, "vapor");
-+
-+    const char expected[] = "true";
-+
-+    if (client_write_buf_idx != sizeof(expected) - 1) {
-+        lgtd_errx(
-+            1,
-+            "%d bytes written, expected %lu "
-+            "(got %.*s instead of %s)",
-+            client_write_buf_idx, sizeof(expected) - 1UL,
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+
-+    if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) {
-+        lgtd_errx(
-+            1, "got %.*s instead of %s",
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+
-+    if (!device_list_free_called) {
-+        lgtd_errx(1, "the list of devices hasn't been freed");
-+    }
-+    if (!send_to_device_called) {
-+        lgtd_errx(1, "nothing was send to any device");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/proto/test_proto_untag_tag_does_not_exist.c b/tests/core/proto/test_proto_untag_tag_does_not_exist.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/proto/test_proto_untag_tag_does_not_exist.c
-@@ -0,0 +1,90 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_daemon.h"
-+#include "mock_gateway.h"
-+#include "tests_utils.h"
-+
-+#define MOCKED_ROUTER_TARGETS_TO_DEVICES
-+#define MOCKED_ROUTER_SEND_TO_DEVICE
-+#define MOCKED_ROUTER_DEVICE_LIST_FREE
-+#include "tests_proto_utils.h"
-+
-+static bool device_list_free_called = false;
-+
-+void
-+lgtd_router_device_list_free(struct lgtd_router_device_list *devices)
-+{
-+    (void)devices;
-+
-+    device_list_free_called = true;
-+}
-+
-+static bool targets_to_devices_called = false;
-+
-+struct lgtd_router_device_list *
-+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets)
-+{
-+    (void)targets;
-+
-+    targets_to_devices_called = true;
-+
-+    static struct lgtd_router_device_list devices =
-+        SLIST_HEAD_INITIALIZER(&devices);
-+
-+    return &devices;
-+}
-+
-+static bool send_to_device_called = false;
-+
-+void
-+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb,
-+                           enum lgtd_lifx_packet_type pkt_type,
-+                           void *pkt)
-+{
-+    (void)bulb;
-+    (void)pkt_type;
-+    (void)pkt;
-+    send_to_device_called = true;
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-+
-+    struct lgtd_proto_target_list *targets;
-+    targets = lgtd_tests_build_target_list("*", NULL);
-+
-+    lgtd_proto_untag(&client, targets, "vapor");
-+
-+    const char expected[] = "true";
-+
-+    if (client_write_buf_idx != sizeof(expected) - 1) {
-+        lgtd_errx(
-+            1,
-+            "%d bytes written, expected %lu "
-+            "(got %.*s instead of %s)",
-+            client_write_buf_idx, sizeof(expected) - 1UL,
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+    if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) {
-+        lgtd_errx(
-+            1, "got %.*s instead of %s",
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+
-+    if (targets_to_devices_called) {
-+        lgtd_errx(1, "unexpected call to targets_to_devices");
-+    }
-+    if (device_list_free_called) {
-+        lgtd_errx(1, "nothing should have been freed");
-+    }
-+    if (send_to_device_called) {
-+        lgtd_errx(1, "nothing should have been sent to any device");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/core/proto/tests_proto_utils.h b/tests/core/proto/tests_proto_utils.h
---- a/tests/core/proto/tests_proto_utils.h
-+++ b/tests/core/proto/tests_proto_utils.h
-@@ -36,6 +36,18 @@
- }
- #endif
- 
-+#ifndef MOCKED_ROUTER_SEND_TO_DEVICE
-+void
-+lgtd_router_send_to_device(struct lgtd_lifx_bulb *bulb,
-+                           enum lgtd_lifx_packet_type pkt_type,
-+                           void *pkt)
-+{
-+    (void)bulb;
-+    (void)pkt_type;
-+    (void)pkt;
-+}
-+#endif
-+
- #ifndef MOCKED_ROUTER_SEND
- bool
- lgtd_router_send(const struct lgtd_proto_target_list *targets,
-diff --git a/tests/core/tests_shims.h b/tests/core/tests_shims.h
-new file mode 100644
---- /dev/null
-+++ b/tests/core/tests_shims.h
-@@ -0,0 +1,23 @@
-+#pragma once
-+
-+struct lgtd_opts lgtd_opts = {
-+    .foreground = false,
-+    .log_timestamps = false,
-+    .verbosity = LGTD_DEBUG
-+};
-+
-+struct event_base *lgtd_ev_base = NULL;
-+
-+const char *lgtd_binds = NULL;
-+
-+void
-+lgtd_cleanup(void)
-+{
-+}
-+
-+#ifndef MOCKED_DAEMON_UPDATE_PROCTITLE
-+void
-+lgtd_daemon_update_proctitle(void)
-+{
-+}
-+#endif
-diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
---- a/tests/core/tests_utils.c
-+++ b/tests/core/tests_utils.c
-@@ -118,8 +118,11 @@
-     struct lgtd_lifx_site *site = calloc(1, sizeof(*site));
-     site->gw = gw;
-     site->tag_id = tag_id;
-+    LIST_INSERT_HEAD(&tag->sites, site, link);
-+
-     gw->tags[tag_id] = tag;
--    LIST_INSERT_HEAD(&tag->sites, site, link);
-+    gw->tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-+
-     return site;
- }
- 
-diff --git a/tests/lifx/bulb/CMakeLists.txt b/tests/lifx/bulb/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/bulb/CMakeLists.txt
-@@ -0,0 +1,29 @@
-+INCLUDE_DIRECTORIES(
-+    ${CMAKE_CURRENT_SOURCE_DIR}
-+    ${CMAKE_CURRENT_BINARY_DIR}
-+)
-+
-+ADD_CORE_LIBRARY(
-+    test_lifx_bulb_core STATIC
-+    ${LIGHTSD_SOURCE_DIR}/core/log.c
-+    ${LIGHTSD_SOURCE_DIR}/core/router.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-+)
-+
-+ADD_LIBRARY(
-+    test_lifx_bulb STATIC
-+    ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-+    ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-+)
-+
-+FUNCTION(ADD_BULB_TEST TEST_SOURCE)
-+    ADD_TEST_FROM_C_SOURCES(
-+        ${TEST_SOURCE} test_lifx_bulb_core test_lifx_bulb
-+    )
-+ENDFUNCTION()
-+
-+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
-+FOREACH(TEST ${TESTS})
-+    ADD_BULB_TEST(${TEST})
-+ENDFOREACH()
-diff --git a/tests/lifx/bulb/test_bulb_close.c b/tests/lifx/bulb/test_bulb_close.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/bulb/test_bulb_close.c
-@@ -0,0 +1,33 @@
-+#include "bulb.c"
-+
-+#include "mock_gateway.h"
-+
-+int
-+main(void)
-+{
-+    struct lgtd_lifx_gateway gw;
-+    uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 };
-+    struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr);
-+
-+    bulb->state.power = LGTD_LIFX_POWER_ON;
-+    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, 1);
-+
-+    lgtd_lifx_bulb_close(bulb);
-+
-+    if (!RB_EMPTY(&lgtd_lifx_bulbs_table)) {
-+        errx(1, "The bulbs table should be empty!");
-+    }
-+
-+    if (LGTD_STATS_GET(bulbs) != 0) {
-+        errx(1, "The bulbs counter is %d (expected 0)", LGTD_STATS_GET(bulbs));
-+    }
-+
-+    if (LGTD_STATS_GET(bulbs_powered_on) != 0) {
-+        errx(
-+            1, "The powered on bulbs counter is %d (expected 0)",
-+            LGTD_STATS_GET(bulbs_powered_on)
-+        );
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/bulb/test_bulb_open.c b/tests/lifx/bulb/test_bulb_open.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/bulb/test_bulb_open.c
-@@ -0,0 +1,44 @@
-+#include "bulb.c"
-+
-+#include "mock_gateway.h"
-+
-+int
-+main(void)
-+{
-+    struct lgtd_lifx_gateway gw;
-+    uint8_t bulb_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1, 0 };
-+    lgtd_time_mono_t now = lgtd_time_monotonic_msecs();
-+    struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_open(&gw, bulb_addr);
-+
-+    if (!bulb) {
-+        errx(1, "lgtd_lifx_bulb_open didn't return any bulb");
-+    }
-+
-+    if (memcmp(bulb->addr, bulb_addr, LGTD_LIFX_ADDR_LENGTH)) {
-+        errx(
-+            1, "got bulb addr %s (expected %s)",
-+            lgtd_addrtoa(bulb->addr), lgtd_addrtoa(bulb_addr)
-+        );
-+    }
-+
-+    if (bulb->gw != &gw) {
-+        errx(1, "got bulb gateway %p (expected %p)", bulb->gw, &gw);
-+    }
-+
-+    if (lgtd_lifx_bulb_get(bulb_addr) != bulb) {
-+        errx(1, "the new bulb can't be found");
-+    }
-+
-+    if (bulb->last_light_state_at < now) {
-+        errx(
-+            1, "got bulb->last_light_state_at %ju (expected >= %ju)",
-+            (uintmax_t)bulb->last_light_state_at, (uintmax_t)now
-+        );
-+    }
-+
-+    if (LGTD_STATS_GET(bulbs) != 1) {
-+        errx(1, "bulbs counter is %d (expected 1)", LGTD_STATS_GET(bulbs));
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/bulb/test_bulb_set_light_state.c b/tests/lifx/bulb/test_bulb_set_light_state.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/bulb/test_bulb_set_light_state.c
-@@ -0,0 +1,92 @@
-+#include "bulb.c"
-+
-+#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS
-+#include "mock_gateway.h"
-+
-+static int update_tag_refcouts_call_counts = 0;
-+
-+void
-+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw,
-+                                       uint64_t bulb_tags,
-+                                       uint64_t pkt_tags)
-+{
-+    if (gw != (void *)0xdeaf) {
-+        errx(1, "got wrong gw %p (expected 0xdeaf)", gw);
-+    }
-+
-+    if (pkt_tags != 0xfeed) {
-+        errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags);
-+    }
-+
-+    if (!update_tag_refcouts_call_counts) {
-+        if (bulb_tags != 0x2a) {
-+            errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags);
-+        }
-+    } else {
-+        if (bulb_tags != 0xfeed) {
-+            errx(1, "got bulb_tags %#jx (expected 0xfeed)", (uintmax_t)bulb_tags);
-+        }
-+    }
-+
-+    update_tag_refcouts_call_counts++;
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_lifx_bulb bulb = {
-+        .state = {
-+            .hue = 54321,
-+            .brightness = UINT16_MAX,
-+            .kelvin = 12345,
-+            .dim = 808,
-+            .power = LGTD_LIFX_POWER_OFF,
-+            .label = "lair",
-+            .tags = 0x2a
-+        },
-+        .gw = (void *)0xdeaf
-+    };
-+
-+    struct lgtd_lifx_light_state new_state = {
-+        .hue = 22222,
-+        .brightness = UINT16_MAX / 2,
-+        .kelvin = 54321,
-+        .dim = 303,
-+        .power = LGTD_LIFX_POWER_ON,
-+        .label = "caverne",
-+        .tags = 0xfeed
-+    };
-+
-+    lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015);
-+    if (memcmp(&bulb.state, &new_state, sizeof(new_state))) {
-+        errx(1, "new light state incorrectly set");
-+    }
-+    if (LGTD_STATS_GET(bulbs_powered_on) != 1) {
-+        errx(
-+            1, "unexpected bulbs_powered_on counter value %d (expected 1)",
-+            LGTD_STATS_GET(bulbs_powered_on)
-+        );
-+    }
-+    if (bulb.last_light_state_at != 2015) {
-+        errx(
-+            1, "got bulb.last_light_state = %jx (expected 2015)",
-+            (uintmax_t)bulb.last_light_state_at
-+        );
-+    }
-+    if (update_tag_refcouts_call_counts != 1) {
-+        errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called");
-+    }
-+
-+    lgtd_lifx_bulb_set_light_state(&bulb, &new_state, 2015);
-+    if (update_tag_refcouts_call_counts != 2) {
-+        errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called");
-+    }
-+    if (LGTD_STATS_GET(bulbs_powered_on) != 1) {
-+        errx(
-+            1, "unexpected bulbs_powered_on counter value %d (expected 1)",
-+            LGTD_STATS_GET(bulbs_powered_on)
-+        );
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/bulb/test_bulb_set_power_state.c b/tests/lifx/bulb/test_bulb_set_power_state.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/bulb/test_bulb_set_power_state.c
-@@ -0,0 +1,39 @@
-+#include "bulb.c"
-+
-+#include "mock_gateway.h"
-+
-+int
-+main(void)
-+{
-+    struct lgtd_lifx_bulb bulb = {
-+        .state = {
-+            .hue = 54321,
-+            .brightness = UINT16_MAX,
-+            .kelvin = 12345,
-+            .dim = 808,
-+            .power = LGTD_LIFX_POWER_OFF,
-+            .label = "lair",
-+            .tags = 0x2a
-+        },
-+        .gw = (void *)0xdeaf
-+    };
-+    struct lgtd_lifx_light_state new_state;
-+    memcpy(&new_state, &bulb.state, sizeof(new_state));
-+    new_state.power = LGTD_LIFX_POWER_ON;
-+
-+
-+    for (int i = 0; i != 2; i++) {
-+        lgtd_lifx_bulb_set_power_state(&bulb, LGTD_LIFX_POWER_ON);
-+        if (memcmp(&bulb.state, &new_state, sizeof(new_state))) {
-+            errx(1, "new light state incorrectly set");
-+        }
-+        if (LGTD_STATS_GET(bulbs_powered_on) != 1) {
-+            errx(
-+                1, "unexpected bulbs_powered_on counter value %d (expected 1)",
-+                LGTD_STATS_GET(bulbs_powered_on)
-+            );
-+        }
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/bulb/test_bulb_set_tags.c b/tests/lifx/bulb/test_bulb_set_tags.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/bulb/test_bulb_set_tags.c
-@@ -0,0 +1,50 @@
-+#include "bulb.c"
-+
-+#define MOCKED_LGTD_LIFX_GATEWAY_UPDATE_TAG_REFCOUNTS
-+#include "mock_gateway.h"
-+
-+static bool update_tag_refcouts_called = false;
-+
-+void
-+lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw,
-+                                       uint64_t bulb_tags,
-+                                       uint64_t pkt_tags)
-+{
-+    if (gw != (void *)0xdeaf) {
-+        errx(1, "got wrong gw %p (expected 0xdeaf)", gw);
-+    }
-+
-+    if (bulb_tags != 0x2a) {
-+        errx(1, "got bulb_tags %#jx (expected 0x2a)", (uintmax_t)bulb_tags);
-+    }
-+
-+    if (pkt_tags != 0xfeed) {
-+        errx(1, "got pkt_tags %#jx (expected 0xfeed)", (uintmax_t)pkt_tags);
-+    }
-+
-+    update_tag_refcouts_called = true;
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_lifx_bulb bulb = {
-+        .state = { .tags = 0x2a },
-+        .gw = (void *)0xdeaf
-+    };
-+
-+    lgtd_lifx_bulb_set_tags(&bulb, 0xfeed);
-+
-+    if (bulb.state.tags != 0xfeed) {
-+        errx(
-+            1, "got bulb.state.tags = %#jx (expected 0xfeed)",
-+            (uintmax_t)bulb.state.tags
-+        );
-+    }
-+
-+    if (!update_tag_refcouts_called) {
-+        errx(1, "lgtd_lifx_gateway_update_tag_refcounts wasn't called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id.c b/tests/lifx/gateway/test_gateway_allocate_tag_id.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/gateway/test_gateway_allocate_tag_id.c
-@@ -0,0 +1,102 @@
-+#include "gateway.c"
-+
-+#include <string.h>
-+
-+#define MOCKED_LIFX_TAGGING_INCREF
-+#include "test_gateway_utils.h"
-+
-+static bool tagging_incref_called = false;
-+
-+struct lgtd_lifx_tag *
-+lgtd_lifx_tagging_incref(const char *label,
-+                         struct lgtd_lifx_gateway *gw,
-+                         int tag_id)
-+{
-+    if (!label) {
-+        errx(1, "missing tag label");
-+    }
-+    if (!gw) {
-+        errx(1, "missing gateway");
-+    }
-+    if (tag_id > 2) {
-+        errx(1, "got tag_id %d but expected < 3", tag_id);
-+    }
-+
-+    struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label);
-+    if (!tag) {
-+        tag = calloc(1, sizeof(*tag));
-+        strcpy(tag->label, label);
-+        struct lgtd_lifx_site *site = calloc(1, sizeof(*site));
-+        site->gw = gw;
-+        site->tag_id = tag_id;
-+        LIST_INSERT_HEAD(&tag->sites, site, link);
-+    }
-+
-+    tagging_incref_called = true;
-+
-+    return tag;
-+}
-+
-+int
-+main(void)
-+{
-+    lgtd_lifx_wire_load_packet_infos_map();
-+
-+    struct lgtd_lifx_gateway gw;
-+    memset(&gw, 0, sizeof(gw));
-+
-+    struct lgtd_lifx_packet_header hdr;
-+    memset(&hdr, 0, sizeof(hdr));
-+
-+    uint64_t expected_tag_ids = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0);
-+
-+    lgtd_lifx_gateway_allocate_tag_id(&gw, 0, "test");
-+    if (!gw.tags[0]) {
-+        errx(1, "gw.tag_ids[0] shouldn't be NULL");
-+    }
-+    if (strcmp(gw.tags[0]->label, "test")) {
-+        errx(
-+            1, "unexpected tag %.*s (expected test)",
-+            (int)sizeof(gw.tags[0]->label), gw.tags[0]->label
-+        );
-+    }
-+    if (gw.tag_ids != LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(0)) {
-+        errx(
-+            1, "tag_ids = %jx (expected %jx)",
-+            (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids
-+        );
-+    }
-+    if (!tagging_incref_called) {
-+        errx(1, "lgtd_lifx_tagging_incref should have been called");
-+    }
-+    tagging_incref_called = false;
-+
-+    for (int i = 1; i != 3; i++) {
-+        int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge");
-+        if (tag_id < 1) {
-+            errx(1, "no tag_id was allocated (received tag_id %d)", tag_id);
-+        }
-+        if (!gw.tags[tag_id]) {
-+            errx(1, "gw.tag_ids[%d] shouldn't be NULL", i);
-+        }
-+        if (strcmp(gw.tags[tag_id]->label, "lounge")) {
-+            errx(
-+                1, "unexpected tag %.*s (expected lounge)",
-+                (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label
-+            );
-+        }
-+        expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-+        if (gw.tag_ids != expected_tag_ids) {
-+            errx(
-+                1, "tag_ids = %jx (expected %jx)",
-+                (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids
-+            );
-+        }
-+        if (!tagging_incref_called) {
-+            errx(1, "lgtd_lifx_tagging_incref should have been called");
-+        }
-+        tagging_incref_called = false;
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c
-@@ -0,0 +1,89 @@
-+
-+#include <string.h>
-+
-+#include "gateway.c"
-+
-+#define MOCKED_LIFX_TAGGING_INCREF
-+#include "test_gateway_utils.h"
-+
-+static bool tagging_incref_called = false;
-+
-+struct lgtd_lifx_tag *
-+lgtd_lifx_tagging_incref(const char *label,
-+                         struct lgtd_lifx_gateway *gw,
-+                         int tag_id)
-+{
-+    if (!label) {
-+        errx(1, "missing tag label");
-+    }
-+    if (!gw) {
-+        errx(1, "missing gateway");
-+    }
-+    if (tag_id < 0) {
-+        errx(1, "got tag_id %d but expected >= 0", tag_id);
-+    }
-+
-+    struct lgtd_lifx_tag *tag = lgtd_lifx_tagging_find_tag(label);
-+    if (!tag) {
-+        tag = calloc(1, sizeof(*tag));
-+        strcpy(tag->label, label);
-+        struct lgtd_lifx_site *site = calloc(1, sizeof(*site));
-+        site->gw = gw;
-+        site->tag_id = tag_id;
-+        LIST_INSERT_HEAD(&tag->sites, site, link);
-+    }
-+
-+    tagging_incref_called = true;
-+
-+    return tag;
-+}
-+
-+int
-+main(void)
-+{
-+    lgtd_lifx_wire_load_packet_infos_map();
-+
-+    struct lgtd_lifx_gateway gw;
-+    memset(&gw, 0, sizeof(gw));
-+
-+    struct lgtd_lifx_packet_header hdr;
-+    memset(&hdr, 0, sizeof(hdr));
-+
-+    uint64_t expected_tag_ids = 0;
-+    for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) {
-+        int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge");
-+        if (tag_id < 0) {
-+            errx(1, "no tag_id was allocated (received tag_id %d)", tag_id);
-+        }
-+        if (!gw.tags[tag_id]) {
-+            errx(1, "gw.tag_ids[%d] shouldn't be NULL", i);
-+        }
-+        if (strcmp(gw.tags[tag_id]->label, "lounge")) {
-+            errx(
-+                1, "unexpected tag %.*s (expected lounge)",
-+                (int)sizeof(gw.tags[tag_id]->label), gw.tags[tag_id]->label
-+            );
-+        }
-+        expected_tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-+        if (gw.tag_ids != expected_tag_ids) {
-+            errx(
-+                1, "tag_ids = %jx (expected %jx)",
-+                (uintmax_t)gw.tag_ids, (uintmax_t)expected_tag_ids
-+            );
-+        }
-+        if (!tagging_incref_called) {
-+            errx(1, "lgtd_lifx_tagging_incref should have been called");
-+        }
-+        tagging_incref_called = false;
-+    }
-+
-+    int tag_id = lgtd_lifx_gateway_allocate_tag_id(&gw, -1, "lounge");
-+    if (tag_id != -1) {
-+        errx(1, "tag_ids full but tag_id %d was allocated", tag_id);
-+    }
-+    if (tagging_incref_called) {
-+        errx(1, "lgtd_lifx_tagging_incref should not have been called");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/gateway/test_gateway_update_tag_refcounts.c b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c
-@@ -0,0 +1,106 @@
-+#include "gateway.c"
-+
-+#include "test_gateway_utils.h"
-+
-+int
-+main(void)
-+{
-+    lgtd_lifx_wire_load_packet_infos_map();
-+
-+    struct lgtd_lifx_gateway gw;
-+    memset(&gw, 0, sizeof(gw));
-+
-+    lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 0);
-+    for (int i = 0; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) {
-+        if (gw.tag_refcounts[i]) {
-+            errx(
-+                1, "gw.tag_refcounts[%d] was %d, (expected 0)",
-+                i, gw.tag_refcounts[i]
-+            );
-+        }
-+    }
-+
-+    for (int n = 1; n != 3; n++) {
-+        lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 1);
-+        if (gw.tag_refcounts[0] != n) {
-+            errx(
-+                1, "gw.tag_refcounts[0] was %d (expected %d)",
-+                gw.tag_refcounts[0], n
-+            );
-+        }
-+        for (int i = 1; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) {
-+            if (gw.tag_refcounts[i]) {
-+                errx(
-+                    1, "gw.tag_refcounts[%d] was %d (expected 0)",
-+                    i, gw.tag_refcounts[i]
-+                );
-+            }
-+        }
-+    }
-+
-+    lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 2);
-+    gw.tag_ids = 0x2;
-+
-+    for (int n = 1; n >= 0; n--) {
-+        lgtd_lifx_gateway_update_tag_refcounts(&gw, 1, 0);
-+        if (gw.tag_refcounts[0] != n) {
-+            errx(
-+                1, "gw.tag_refcounts[0] was %d (expected %d)",
-+                gw.tag_refcounts[0], n - 1
-+            );
-+        }
-+        if (gw.tag_refcounts[1] != 1) {
-+            errx(
-+                1, "gw.tag_refcounts[1] was %d (expected 1)",
-+                gw.tag_refcounts[1]
-+            );
-+        }
-+        for (int i = 2; i != LGTD_LIFX_GATEWAY_MAX_TAGS; i++) {
-+            if (gw.tag_refcounts[i]) {
-+                errx(
-+                    1, "gw.tag_refcounts[%d] was %d (expected 0)",
-+                    i, gw.tag_refcounts[i]
-+                );
-+            }
-+        }
-+    }
-+    if (gw.pkt_ring[0].type != LGTD_LIFX_SET_TAG_LABELS) {
-+        errx(1, "SET_TAG_LABELS should have been enqueued on the gateway");
-+    }
-+
-+    struct lgtd_lifx_packet_tag_labels *pkt =
-+        (void *)&gw_write_buf[sizeof(struct lgtd_lifx_packet_header)];
-+    uint64_t tags = le64toh(pkt->tags);
-+    if (tags != ~2ULL) {
-+        errx(
-+            1, "tags on LGTD_LIFX_SET_TAG_LABELS was %#jx (expected %#jx)",
-+            (uintmax_t)tags, (uintmax_t)~2ULL
-+        );
-+    }
-+    const char blank_label[LGTD_LIFX_LABEL_SIZE] = { 0 };
-+    if (memcmp(pkt->label, blank_label, LGTD_LIFX_LABEL_SIZE)) {
-+        errx(
-+            1, "label on LGTD_LIFX_SET_TAG_LABELS should be "
-+            "all zero but got %.*s", LGTD_LIFX_LABEL_SIZE, pkt->label
-+        );
-+    }
-+
-+    for (int n = 0; n != UINT8_MAX; n++) {
-+        lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4);
-+    }
-+    if (gw.tag_refcounts[2] != UINT8_MAX) {
-+        errx(
-+            1, "gw.tag_refcounts[2] was %d (expected %d)",
-+            gw.tag_refcounts[2], UINT8_MAX
-+        );
-+    }
-+    lgtd_lifx_gateway_update_tag_refcounts(&gw, 0, 4);
-+    if (gw.tag_refcounts[2] != UINT8_MAX) {
-+        errx(
-+            1, "gw.tag_refcounts[2] was %d (expected %d)",
-+            gw.tag_refcounts[2], UINT8_MAX
-+        );
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h
---- a/tests/lifx/mock_gateway.h
-+++ b/tests/lifx/mock_gateway.h
-@@ -81,6 +81,18 @@
- }
- #endif
- 
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_TAGS
-+void
-+lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw,
-+                              const struct lgtd_lifx_packet_header *hdr,
-+                              const struct lgtd_lifx_packet_tags *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
- #ifndef MOCKED_LGTD_LIFX_GATEWAY_DEALLOCATE_TAG_ID
- void
- lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id)
-diff --git a/tests/lifx/wire_proto/test_wire_proto_utils.h b/tests/lifx/wire_proto/test_wire_proto_utils.h
---- a/tests/lifx/wire_proto/test_wire_proto_utils.h
-+++ b/tests/lifx/wire_proto/test_wire_proto_utils.h
-@@ -35,3 +35,12 @@
-     (void)hdr;
-     (void)pkt;
- }
-+
-+void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *gw,
-+                                   const struct lgtd_lifx_packet_header *hdr,
-+                                   const struct lgtd_lifx_packet_tags *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
--- a/fix_crash_in_get_light_state_when_tag_does_not_exist.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-# HG changeset patch
-# Parent  97f7fd10087f08547f48c20644461f2356e7399b
-Handle bulbs with bugged tags bitfields
-
-I'm getting that on the LIFX White 800 for tag and untag I'm afraid
-something is weird with their firmare.
-
-diff --git a/core/proto.c b/core/proto.c
---- a/core/proto.c
-+++ b/core/proto.c
-@@ -209,10 +209,20 @@
-         bool comma = false;
-         int tag_id;
-         LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) {
--            lgtd_client_write_string(client, comma ? ",\"" : "\"");
--            lgtd_client_write_string(client, bulb->gw->tags[tag_id]->label);
--            lgtd_client_write_string(client, "\"");
--            comma = true;
-+            if (LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id) & bulb->gw->tag_ids) {
-+                lgtd_client_write_string(client, comma ? ",\"" : "\"");
-+                lgtd_client_write_string(client, bulb->gw->tags[tag_id]->label);
-+                lgtd_client_write_string(client, "\"");
-+                comma = true;
-+            } else {
-+                lgtd_warnx(
-+                    "tag_id %d on bulb %.*s (%s) doesn't "
-+                    "exist on gw [%s]:%hu (site %s)",
-+                    tag_id, (int)sizeof(bulb->state.label), bulb->state.label,
-+                    lgtd_addrtoa(bulb->addr), bulb->gw->ip_addr, bulb->gw->port,
-+                    lgtd_addrtoa(bulb->gw->site.as_array)
-+                );
-+            }
-         }
- 
-         lgtd_client_write_string(
-diff --git a/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c b/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c
-new file mode 100644
---- /dev/null
-+++ b/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c
-@@ -0,0 +1,129 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#include "mock_daemon.h"
-+#include "mock_gateway.h"
-+#include "tests_utils.h"
-+
-+#define MOCKED_ROUTER_TARGETS_TO_DEVICES
-+#define MOCKED_ROUTER_DEVICE_LIST_FREE
-+#include "tests_proto_utils.h"
-+
-+static bool device_list_free_called = false;
-+
-+void
-+lgtd_router_device_list_free(struct lgtd_router_device_list *devices)
-+{
-+    if (!devices) {
-+        lgtd_errx(1, "the device list must be passed");
-+    }
-+
-+    device_list_free_called = true;
-+}
-+
-+struct lgtd_router_device_list *
-+lgtd_router_targets_to_devices(const struct lgtd_proto_target_list *targets)
-+{
-+    if (targets != (void *)0x2a) {
-+        lgtd_errx(1, "unexpected targets list");
-+    }
-+
-+    static struct lgtd_router_device_list devices =
-+        SLIST_HEAD_INITIALIZER(&devices);
-+
-+    static struct lgtd_lifx_gateway gw_bulb_1 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_1.bulbs)
-+    };
-+    static struct lgtd_lifx_bulb bulb_1 = {
-+        .addr = { 1, 2, 3, 4, 5 },
-+        .state = {
-+            .hue = 0xaaaa,
-+            .saturation = 0xffff,
-+            .brightness = 0xbbbb,
-+            .kelvin = 3600,
-+            .label = "wave",
-+            .power = LGTD_LIFX_POWER_ON,
-+            .tags = 5
-+        },
-+        .gw = &gw_bulb_1
-+    };
-+    static struct lgtd_router_device device_1 = { .device = &bulb_1 };
-+    SLIST_INSERT_HEAD(&devices, &device_1, link);
-+
-+    struct lgtd_lifx_tag *gw_2_tag_1 = lgtd_tests_insert_mock_tag("vapor");
-+    struct lgtd_lifx_tag *gw_2_tag_2 = lgtd_tests_insert_mock_tag("d^-^b");
-+    struct lgtd_lifx_tag *gw_2_tag_3 = lgtd_tests_insert_mock_tag("wave~");
-+    static struct lgtd_lifx_gateway gw_bulb_2 = {
-+        .bulbs = LIST_HEAD_INITIALIZER(&gw_bulb_2.bulbs),
-+        .tag_ids = 0x7
-+    };
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_1, &gw_bulb_2, 0);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_2, &gw_bulb_2, 1);
-+    lgtd_tests_add_tag_to_gw(gw_2_tag_3, &gw_bulb_2, 2);
-+    static struct lgtd_lifx_bulb bulb_2 = {
-+        .addr = { 5, 4, 3, 2, 1 },
-+        .state = {
-+            .hue = 0x0000,
-+            .saturation = 0x0000,
-+            .brightness = 0xffff,
-+            .kelvin = 4000,
-+            .label = "",
-+            .power = LGTD_LIFX_POWER_OFF,
-+            .tags = 0x3
-+        },
-+        .gw = &gw_bulb_2
-+    };
-+    static struct lgtd_router_device device_2 = { .device = &bulb_2 };
-+    SLIST_INSERT_HEAD(&devices, &device_2, link);
-+
-+    return &devices;
-+}
-+
-+int
-+main(void)
-+{
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
-+    struct lgtd_proto_target_list *targets = (void *)0x2a;
-+
-+    lgtd_proto_get_light_state(&client, targets);
-+
-+    const char expected[] = (
-+        "["
-+            "{"
-+                "\"hsbk\":[0,0,1,4000],"
-+                "\"power\":false,"
-+                "\"label\":\"\","
-+                "\"tags\":[\"vapor\",\"d^-^b\"]"
-+            "},"
-+            "{"
-+                "\"hsbk\":[240,1,0.733333,3600],"
-+                "\"power\":true,"
-+                "\"label\":\"wave\","
-+                "\"tags\":[]"
-+            "}"
-+        "]"
-+    );
-+
-+    if (client_write_buf_idx != sizeof(expected) - 1) {
-+        lgtd_errx(
-+            1,
-+            "%d bytes written, expected %lu "
-+            "(got %.*s instead of %s)",
-+            client_write_buf_idx, sizeof(expected) - 1UL,
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+
-+    if (memcmp(expected, client_write_buf, sizeof(expected) - 1)) {
-+        lgtd_errx(
-+            1, "got %.*s instead of %s",
-+            client_write_buf_idx, client_write_buf, expected
-+        );
-+    }
-+
-+    if (!device_list_free_called) {
-+        lgtd_errx(1, "the list of devices hasn't been freed");
-+    }
-+
-+    return 0;
-+}
--- a/fix_lifx_wire_float_endian_functions_naming.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-# HG changeset patch
-# Parent  493a81b724f08c6d746c3879bafd86e7f05342fa
-Fix a couple function names
-
-diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -382,7 +382,7 @@
-     pkt->brightness = htole16(pkt->brightness);
-     pkt->kelvin = htole16(pkt->kelvin);
-     pkt->period = htole32(pkt->period);
--    pkt->cycles = lifx_wire_htolefloat(pkt->cycles);
-+    pkt->cycles = lgtd_lifx_wire_htolefloat(pkt->cycles);
-     pkt->skew_ratio = htole16(pkt->skew_ratio);
- }
- 
-diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
---- a/lifx/wire_proto.h
-+++ b/lifx/wire_proto.h
-@@ -26,14 +26,14 @@
- typedef float    floatle_t;
- 
- static inline floatle_t
--lifx_wire_htolefloat(float f)
-+lgtd_lifx_wire_htolefloat(float f)
- {
-     *(uint32_t *)&f = htole32(*(uint32_t *)&f);
-     return f;
- }
- 
- static inline floatle_t
--lifx_wire_lefloattoh(float f)
-+lgtd_lifx_wire_lefloattoh(float f)
- {
-     *(uint32_t *)&f = le32toh(*(uint32_t *)&f);
-     return f;
-diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c
---- a/tests/core/proto/test_proto_set_waveform.c
-+++ b/tests/core/proto/test_proto_set_waveform.c
-@@ -36,7 +36,7 @@
-     int brightness = le16toh(waveform->brightness);
-     int kelvin = le16toh(waveform->kelvin);
-     int period = le32toh(waveform->period);
--    float cycles = lifx_wire_lefloattoh(waveform->cycles);
-+    float cycles = lgtd_lifx_wire_lefloattoh(waveform->cycles);
-     int skew_ratio = le16toh(waveform->skew_ratio);
- 
-     if (waveform_type != LGTD_LIFX_WAVEFORM_SAW) {
-diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c
---- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c
-+++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c
-@@ -36,7 +36,7 @@
-     int brightness = le16toh(waveform->brightness);
-     int kelvin = le16toh(waveform->kelvin);
-     int period = le32toh(waveform->period);
--    float cycles = lifx_wire_lefloattoh(waveform->cycles);
-+    float cycles = lgtd_lifx_wire_lefloattoh(waveform->cycles);
-     int skew_ratio = le16toh(waveform->skew_ratio);
- 
-     if (waveform_type != LGTD_LIFX_WAVEFORM_SAW) {
--- a/fix_set_power_state.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# HG changeset patch
-# Parent  b5f496df71441bfe4bb153a027acf832255ea81a
-Handle intermediate values between POWER_STATE_ON and OFF from the bulbs
-
-It's supposed to be 0xffff or 0 but that's actually not true.
-
-diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -342,16 +342,22 @@
-     pkt->brightness = le16toh(pkt->brightness);
-     pkt->kelvin = le16toh(pkt->kelvin);
-     pkt->dim = le16toh(pkt->dim);
--    pkt->power = le16toh(pkt->power);
-+    // The bulbs actually return power values between 0 and 0xffff, not sure
-+    // what the intermediate values mean, let's pull them down to 0:
-+    if (pkt->power != LGTD_LIFX_POWER_ON) {
-+        pkt->power = LGTD_LIFX_POWER_OFF;
-+    }
-     pkt->tags = le64toh(pkt->tags);
- }
- 
- void
- lgtd_lifx_wire_decode_power_state(struct lgtd_lifx_packet_power_state *pkt)
- {
--    (void)pkt;
-+    assert(pkt);
- 
--    assert(pkt);
-+    if (pkt->power != LGTD_LIFX_POWER_ON) {
-+        pkt->power = LGTD_LIFX_POWER_OFF;
-+    }
- }
- 
- void
--- a/fix_targeting_on_big_endian_architectures.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-# HG changeset patch
-# Parent  75b032c23f91d4ddf3b4842bb0f1b7839fdf7ecf
-Fix tag targeting on big endian platforms
-
-I think that was the last issue on big endian platforms. I can't wait to
-setup a buildbot for it.
-
-diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -246,7 +246,7 @@
-     }
-     if (flags & LGTD_LIFX_TAGGED) {
-         hdr->protocol |= LGTD_LIFX_PROTOCOL_TAGGED;
--        htole64(hdr->target.tags);
-+        hdr->target.tags = htole64(hdr->target.tags);
-     }
-     if (flags & LGTD_LIFX_ACK_REQUIRED) {
-         hdr->flags |= LGTD_LIFX_FLAG_ACK_REQUIRED;
-@@ -267,8 +267,13 @@
-     assert(hdr);
- 
-     hdr->size = le16toh(hdr->size);
--    hdr->protocol = le16toh(hdr->protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK)
--        | (hdr->protocol & LGTD_LIFX_PROTOCOL_FLAGS_MASK);
-+    hdr->protocol = (
-+        le16toh(hdr->protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK)
-+        | (hdr->protocol & LGTD_LIFX_PROTOCOL_FLAGS_MASK)
-+    );
-+    if (hdr->protocol & LGTD_LIFX_PROTOCOL_TAGGED) {
-+        hdr->target.tags = le64toh(hdr->target.tags);
-+    }
-     hdr->at_time = le64toh(hdr->at_time);
-     hdr->packet_type = le16toh(hdr->packet_type);
- }
-diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
---- a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
-+++ b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
-@@ -20,10 +20,10 @@
-     if (le16toh(hdr.size) != 42) {
-         lgtd_errx(1, "size = %hu (expected = 42)", le16toh(hdr.size));
-     }
--    if (le64toh(hdr.target.tags != 0xbad)) {
-+    if (le64toh(hdr.target.tags) != 0xbad) {
-         lgtd_errx(
-             1, "tags = %#jx (expected = 0xbad)",
--            (uintmax_t)le16toh(hdr.target.tags)
-+            (uintmax_t)le64toh(hdr.target.tags)
-         );
-     }
-     if (le16toh(hdr.packet_type) != LGTD_LIFX_ECHO_REQUEST) {
--- a/fix_unused_unused_attribute.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-# HG changeset patch
-# Parent  77e68f94c2d022c1ca8a62bc3e6c5305867b2709
-Remove unused __attribute__((unused))
-
-diff --git a/core/jsonrpc.c b/core/jsonrpc.c
---- a/core/jsonrpc.c
-+++ b/core/jsonrpc.c
-@@ -129,7 +129,7 @@
-     return c == '-' || (c >= '0' && c <= '9');
- }
- 
--static bool __attribute__((unused))
-+static bool
- lgtd_jsonrpc_type_bool(const jsmntok_t *t, const char *json)
- {
-     if (t->type != JSMN_PRIMITIVE) {
--- a/fix_usage_and_version.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-# HG changeset patch
-# Parent  d0442b5bc172c3841cc5a2b577d4ffdf311c9116
-UI/Cosmetic fixes
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -35,7 +35,9 @@
- INCLUDE(AddAllSubdirectories)
- INCLUDE(AddTestFromSources)
- 
--SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -Wextra -Wall -Wstrict-prototypes -std=c99")
-+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${CMAKE_BUILD_TYPE}} -pipe")
-+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wextra -Wall -Wstrict-prototypes -std=c99")
-+SET(CMAKE_C_FLAGS_${CMAKE_BUILD_TYPE} "")
- 
- ADD_DEFINITIONS(
-     # Only relevant for the GNU libc:
-@@ -55,7 +57,7 @@
- IF (CMAKE_BUILD_TYPE MATCHES "DEBUG")
-     ADD_DEFINITIONS("-DQUEUE_MACRO_DEBUG=1")
-     IF (CMAKE_COMPILER_IS_GNUCC)
--        ADD_DEFINITIONS("-g3" "-ggdb")
-+        SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g3 -ggdb")
-     ENDIF ()
- ENDIF ()
- 
-diff --git a/core/lightsd.c b/core/lightsd.c
---- a/core/lightsd.c
-+++ b/core/lightsd.c
-@@ -139,6 +139,7 @@
-         "  [-v,--verbosity debug|info|warning|error]\n",
-         progname
-     );
-+    lgtd_cleanup();
-     exit(0);
- }
- 
-@@ -162,6 +163,10 @@
-     };
-     const char short_opts[] = "l:c:fthv:V";
- 
-+    if (argc == 1) {
-+        lgtd_usage(argv[0]);
-+    }
-+
-     for (int rv = getopt_long(argc, argv, short_opts, long_opts, NULL);
-          rv != -1;
-          rv = getopt_long(argc, argv, short_opts, long_opts, NULL)) {
-@@ -205,7 +210,7 @@
-             }
-             break;
-         case 'V':
--            printf("%s v%s\n", argv[0], LGTD_VERSION);
-+            printf("lightsd v%s\n", LGTD_VERSION);
-             return 0;
-         default:
-             lgtd_usage(argv[0]);
-diff --git a/core/version.h.in b/core/version.h.in
---- a/core/version.h.in
-+++ b/core/version.h.in
-@@ -29,4 +29,9 @@
- 
- #pragma once
- 
--const char LGTD_VERSION[] = "@LGTD_VERSION@";
-+const char LGTD_VERSION[] = (
-+    "@LIGHTSD_VERSION@\n\n"
-+    "cflags: @CMAKE_C_FLAGS@\n"
-+    "proctitle_support: @HAVE_PROCTITLE@\n\n"
-+    "Copyright (c) 2014, 2015, Louis Opter <kalessin@kalessin.fr>"
-+);
--- a/ignore_duplicate_listening_address.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-# HG changeset patch
-# Parent  d8e34f128c677442c5c047793207dbc1664b6c2e
-Allow duplicate listening address in the command line
-
-diff --git a/core/listen.c b/core/listen.c
---- a/core/listen.c
-+++ b/core/listen.c
-@@ -20,6 +20,7 @@
- #include <err.h>
- #include <stdbool.h>
- #include <stdlib.h>
-+#include <string.h>
- #include <unistd.h>
- 
- #include <event2/listener.h>
-@@ -76,6 +77,13 @@
-     assert(addr);
-     assert(port);
- 
-+    struct lgtd_listen *listener;
-+    SLIST_FOREACH(listener, &lgtd_listeners, link) {
-+        if (!strcmp(listener->addr, addr) && listener->port == port) {
-+            return true;
-+        }
-+    }
-+
-     struct evutil_addrinfo *res = NULL, hints = {
-         .ai_family = AF_UNSPEC,
-         .ai_socktype = SOCK_STREAM,
-@@ -91,7 +99,6 @@
-         return false;
-     }
- 
--    struct lgtd_listen *listener;
-     struct evconnlistener *evlistener;
-     for (struct evutil_addrinfo *it = res; it; it = it->ai_next) {
-         evlistener = NULL;
--- a/ignore_pcaps.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-# HG changeset patch
-# Parent  3c1187a6876d8ff634acc7f75b5d8ae23feda534
-Ignore my packet captures directory
-
-I never found myself looking for an older pcap, what we really need is a
-bulb simulator and test scenarios.
-
-diff --git a/.hgignore b/.hgignore
---- a/.hgignore
-+++ b/.hgignore
-@@ -1,3 +1,4 @@
- .*\.sw[a-z]$
- .*\.py[co]$
- ^build
-+^pcaps
--- a/pass_pkt_info_around.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,878 +0,0 @@
-# HG changeset patch
-# Parent  9f4ce61eb90c9c37e29271f680060561a36b6f44
-Pass pkt_info around instead of looking it up twice
-
-And do that spelling fix too.
-
-diff --git a/core/lightsd.c b/core/lightsd.c
---- a/core/lightsd.c
-+++ b/core/lightsd.c
-@@ -220,7 +220,7 @@
-     argc -= optind;
-     argv += optind;
- 
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
-     if (!lgtd_lifx_timer_setup() || !lgtd_lifx_broadcast_setup()) {
-         lgtd_err(1, "can't setup lightsd");
-     }
-diff --git a/core/router.c b/core/router.c
---- a/core/router.c
-+++ b/core/router.c
-@@ -49,21 +49,19 @@
-     struct lgtd_lifx_packet_header hdr;
-     union lgtd_lifx_target target = { .tags = 0 };
- 
--    const struct lgtd_lifx_packet_infos *pkt_infos = NULL;
-+    const struct lgtd_lifx_packet_info *pkt_info = NULL;
-     struct lgtd_lifx_gateway *gw;
-     LIST_FOREACH(gw, &lgtd_lifx_gateways, link) {
--        pkt_infos = lgtd_lifx_wire_setup_header(
-+        pkt_info = lgtd_lifx_wire_setup_header(
-             &hdr,
-             LGTD_LIFX_TARGET_ALL_DEVICES,
-             target,
-             gw->site.as_array,
-             pkt_type
-         );
--        assert(pkt_infos);
-+        assert(pkt_info);
- 
--        lgtd_lifx_gateway_enqueue_packet(
--            gw, &hdr, pkt_type, pkt, pkt_infos->size
--        );
-+        lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_info, pkt);
- 
-         if (pkt_type == LGTD_LIFX_SET_POWER_STATE) {
-             struct lgtd_lifx_bulb *bulb;
-@@ -76,8 +74,8 @@
-         }
-     }
- 
--    if (pkt_infos) {
--        lgtd_info("broadcasting %s", pkt_infos->name);
-+    if (pkt_info) {
-+        lgtd_info("broadcasting %s", pkt_info->name);
-     }
- }
- 
-@@ -91,19 +89,16 @@
-     struct lgtd_lifx_packet_header hdr;
-     union lgtd_lifx_target target = { .addr = bulb->addr };
- 
--    const struct lgtd_lifx_packet_infos *pkt_infos;
--    pkt_infos = lgtd_lifx_wire_setup_header(
-+    const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header(
-         &hdr,
-         LGTD_LIFX_TARGET_DEVICE,
-         target,
-         bulb->gw->site.as_array,
-         pkt_type
-     );
--    assert(pkt_infos);
-+    assert(pkt_info);
- 
--    lgtd_lifx_gateway_enqueue_packet(
--        bulb->gw, &hdr, pkt_type, pkt, pkt_infos->size
--    );
-+    lgtd_lifx_gateway_enqueue_packet(bulb->gw, &hdr, pkt_info, pkt);
- 
-     if (pkt_type == LGTD_LIFX_SET_POWER_STATE) {
-         bulb->dirty_at = lgtd_time_monotonic_msecs();
-@@ -111,7 +106,7 @@
-         bulb->expected_power_on = payload->power;
-     }
- 
--    lgtd_info("sending %s to %s", pkt_infos->name, lgtd_addrtoa(bulb->addr));
-+    lgtd_info("sending %s to %s", pkt_info->name, lgtd_addrtoa(bulb->addr));
- }
- 
- void
-@@ -119,7 +114,7 @@
-                         enum lgtd_lifx_packet_type pkt_type,
-                         void *pkt)
- {
--    const struct lgtd_lifx_packet_infos *pkt_infos = NULL;
-+    const struct lgtd_lifx_packet_info *pkt_info = NULL;
- 
-     struct lgtd_lifx_site *site;
-     LIST_FOREACH(site, &tag->sites, link) {
-@@ -130,18 +125,16 @@
-         union lgtd_lifx_target target;
-         assert(tag == gw->tags[tag_id]);
-         target.tags = LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
--        pkt_infos = lgtd_lifx_wire_setup_header(
-+        pkt_info = lgtd_lifx_wire_setup_header(
-             &hdr,
-             LGTD_LIFX_TARGET_TAGS,
-             target,
-             gw->site.as_array,
-             pkt_type
-         );
--        assert(pkt_infos);
-+        assert(pkt_info);
- 
--        lgtd_lifx_gateway_enqueue_packet(
--            gw, &hdr, pkt_type, pkt, pkt_infos->size
--        );
-+        lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_info, pkt);
- 
-         if (pkt_type == LGTD_LIFX_SET_POWER_STATE) {
-             struct lgtd_lifx_bulb *bulb;
-@@ -156,8 +149,8 @@
-         }
-     }
- 
--    if (pkt_infos) {
--        lgtd_info("sending %s to #%s", pkt_infos->name, tag->label);
-+    if (pkt_info) {
-+        lgtd_info("sending %s to #%s", pkt_info->name, tag->label);
-     }
- }
- 
-@@ -166,25 +159,23 @@
-                           enum lgtd_lifx_packet_type pkt_type,
-                           void *pkt)
- {
--    const struct lgtd_lifx_packet_infos *pkt_infos = NULL;
-+    const struct lgtd_lifx_packet_info *pkt_info = NULL;
- 
-     struct lgtd_lifx_bulb *bulb;
-     RB_FOREACH(bulb, lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table) {
-         if (!strcmp(bulb->state.label, label)) {
-             struct lgtd_lifx_packet_header hdr;
-             union lgtd_lifx_target target = { .addr = bulb->addr };
--            pkt_infos = lgtd_lifx_wire_setup_header(
-+            pkt_info = lgtd_lifx_wire_setup_header(
-                 &hdr,
-                 LGTD_LIFX_TARGET_DEVICE,
-                 target,
-                 bulb->gw->site.as_array,
-                 pkt_type
-             );
--            assert(pkt_infos);
-+            assert(pkt_info);
- 
--            lgtd_lifx_gateway_enqueue_packet(
--                bulb->gw, &hdr, pkt_type, pkt, pkt_infos->size
--            );
-+            lgtd_lifx_gateway_enqueue_packet(bulb->gw, &hdr, pkt_info, pkt);
- 
-             if (pkt_type == LGTD_LIFX_SET_POWER_STATE) {
-                 bulb->dirty_at = lgtd_time_monotonic_msecs();
-@@ -194,8 +185,8 @@
-         }
-     }
- 
--    if (pkt_infos) {
--        lgtd_info("sending %s to %s", pkt_infos->name, label);
-+    if (pkt_info) {
-+        lgtd_info("sending %s to %s", pkt_info->name, label);
-     }
- }
- 
-diff --git a/lifx/broadcast.c b/lifx/broadcast.c
---- a/lifx/broadcast.c
-+++ b/lifx/broadcast.c
-@@ -117,9 +117,9 @@
-             continue;
-         }
- 
--        const struct lgtd_lifx_packet_infos *pkt_infos =
--            lgtd_lifx_wire_get_packet_infos(read.hdr.packet_type);
--        if (!pkt_infos) {
-+        const struct lgtd_lifx_packet_info *pkt_info =
-+            lgtd_lifx_wire_get_packet_info(read.hdr.packet_type);
-+        if (!pkt_info) {
-             lgtd_warnx(
-                 "received unknown packet %#x from [%s]:%hu",
-                 read.hdr.packet_type, peer_addr, peer_port
-@@ -129,7 +129,7 @@
-         if (!(read.hdr.protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE)) {
-             lgtd_warnx(
-                 "received non-addressable packet %s from [%s]:%hu",
--                pkt_infos->name, peer_addr, peer_port
-+                pkt_info->name, peer_addr, peer_port
-             );
-             continue;
-         }
-@@ -145,8 +145,8 @@
-         if (gw) {
-             void *pkt = &read.buf[LGTD_LIFX_PACKET_HEADER_SIZE];
-             gw->last_pkt_at = received_at;
--            pkt_infos->decode(pkt);
--            pkt_infos->handle(gw, &read.hdr, pkt);
-+            pkt_info->decode(pkt);
-+            pkt_info->handle(gw, &read.hdr, pkt);
-         } else {
-             lgtd_warnx(
-                 "got packet from unknown gateway [%s]:%hu", peer_addr, peer_port
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -164,24 +164,24 @@
- static bool
- lgtd_lifx_gateway_send_to_site_impl(struct lgtd_lifx_gateway *gw,
-                                     enum lgtd_lifx_packet_type pkt_type,
--                                    const void *pkt,
--                                    const struct lgtd_lifx_packet_infos **pkt_infos)
-+                                    void *pkt,
-+                                    const struct lgtd_lifx_packet_info **pkt_info)
- {
-     assert(gw);
--    assert(pkt_infos);
-+    assert(pkt_info);
- 
-     struct lgtd_lifx_packet_header hdr;
-     union lgtd_lifx_target target = { .addr = gw->site.as_array };
--    *pkt_infos = lgtd_lifx_wire_setup_header(
-+    *pkt_info = lgtd_lifx_wire_setup_header(
-         &hdr,
-         LGTD_LIFX_TARGET_SITE,
-         target,
-         gw->site.as_array,
-         pkt_type
-     );
--    assert(*pkt_infos);
-+    assert(*pkt_info);
- 
--    lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_type, pkt, (*pkt_infos)->size);
-+    lgtd_lifx_gateway_enqueue_packet(gw, &hdr, *pkt_info, pkt);
- 
-     return true; // FIXME, have real return values on the send paths...
- }
-@@ -189,17 +189,17 @@
- static bool
- lgtd_lifx_gateway_send_to_site_quiet(struct lgtd_lifx_gateway *gw,
-                                      enum lgtd_lifx_packet_type pkt_type,
--                                     const void *pkt)
-+                                     void *pkt)
- {
- 
--    const struct lgtd_lifx_packet_infos *pkt_infos;
-+    const struct lgtd_lifx_packet_info *pkt_info;
-     bool rv = lgtd_lifx_gateway_send_to_site_impl(
--        gw, pkt_type, pkt, &pkt_infos
-+        gw, pkt_type, pkt, &pkt_info
-     );
- 
-     lgtd_debug(
-         "sending %s to site %s",
--        pkt_infos->name, lgtd_addrtoa(gw->site.as_array)
-+        pkt_info->name, lgtd_addrtoa(gw->site.as_array)
-     );
- 
-     return rv; // FIXME, have real return values on the send paths...
-@@ -208,16 +208,16 @@
- bool
- lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-                                enum lgtd_lifx_packet_type pkt_type,
--                               const void *pkt)
-+                               void *pkt)
- {
--    const struct lgtd_lifx_packet_infos *pkt_infos;
-+    const struct lgtd_lifx_packet_info *pkt_info;
-     bool rv = lgtd_lifx_gateway_send_to_site_impl(
--        gw, pkt_type, pkt, &pkt_infos
-+        gw, pkt_type, pkt, &pkt_info
-     );
- 
-     lgtd_info(
-         "sending %s to site %s",
--        pkt_infos->name, lgtd_addrtoa(gw->site.as_array)
-+        pkt_info->name, lgtd_addrtoa(gw->site.as_array)
-     );
- 
-     return rv; // FIXME, have real return values on the send paths...
-@@ -387,13 +387,12 @@
- void
- lgtd_lifx_gateway_enqueue_packet(struct lgtd_lifx_gateway *gw,
-                                  const struct lgtd_lifx_packet_header *hdr,
--                                 enum lgtd_lifx_packet_type pkt_type,
--                                 const void *pkt,
--                                 int pkt_size)
-+                                 const struct lgtd_lifx_packet_info *pkt_info,
-+                                 void *pkt)
- {
-     assert(gw);
-     assert(hdr);
--    assert(pkt_size >= 0 && pkt_size < LGTD_LIFX_MAX_PACKET_SIZE);
-+    assert(pkt_info);
-     assert(!memcmp(hdr->site, gw->site.as_array, LGTD_LIFX_ADDR_LENGTH));
-     assert(gw->pkt_ring_head >= 0);
-     assert(gw->pkt_ring_head < (int)LGTD_ARRAY_SIZE(gw->pkt_ring));
-@@ -401,19 +400,18 @@
-     if (gw->pkt_ring_full) {
-         lgtd_warnx(
-             "dropping packet type %s: packet queue on [%s]:%hu is full",
--            lgtd_lifx_wire_get_packet_infos(pkt_type)->name,
--            gw->ip_addr, gw->port
-+            pkt_info->name, gw->ip_addr, gw->port
-         );
-         return;
-     }
- 
-     evbuffer_add(gw->write_buf, hdr, sizeof(*hdr));
-     if (pkt) {
--        assert((unsigned)pkt_size == le16toh(hdr->size) - sizeof(*hdr));
--        evbuffer_add(gw->write_buf, pkt, pkt_size);
-+        assert(pkt_info->size == le16toh(hdr->size) - sizeof(*hdr));
-+        evbuffer_add(gw->write_buf, pkt, pkt_info->size);
-     }
--    gw->pkt_ring[gw->pkt_ring_head].size = sizeof(*hdr) + pkt_size;
--    gw->pkt_ring[gw->pkt_ring_head].type = pkt_type;
-+    gw->pkt_ring[gw->pkt_ring_head].size = sizeof(*hdr) + pkt_info->size;
-+    gw->pkt_ring[gw->pkt_ring_head].type = pkt_info->type;
-     LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(gw->pkt_ring_head);
-     if (gw->pkt_ring_head == gw->pkt_ring_tail) {
-         gw->pkt_ring_full = true;
-diff --git a/lifx/gateway.h b/lifx/gateway.h
---- a/lifx/gateway.h
-+++ b/lifx/gateway.h
-@@ -91,13 +91,12 @@
- 
- void lgtd_lifx_gateway_enqueue_packet(struct lgtd_lifx_gateway *,
-                                       const struct lgtd_lifx_packet_header *,
--                                      enum lgtd_lifx_packet_type,
--                                      const void *,
--                                      int);
-+                                      const struct lgtd_lifx_packet_info *,
-+                                      void *);
- // This could be on router but it's LIFX specific so I'd rather keep it here:
- bool lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *,
-                                     enum lgtd_lifx_packet_type,
--                                    const void *);
-+                                    void *);
- 
- void lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *, uint64_t, uint64_t);
- 
-diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -48,14 +48,14 @@
-    13, 18,  8, 12,  7,  6,  5, 63
- };
- 
--static struct lgtd_lifx_packet_infos_map lgtd_lifx_packet_infos =
-+static struct lgtd_lifx_packet_info_map lgtd_lifx_packet_info =
-     RB_INITIALIZER(&lgtd_lifx_packets_infos);
- 
- RB_GENERATE_STATIC(
--    lgtd_lifx_packet_infos_map,
--    lgtd_lifx_packet_infos,
-+    lgtd_lifx_packet_info_map,
-+    lgtd_lifx_packet_info,
-     link,
--    lgtd_lifx_packet_infos_cmp
-+    lgtd_lifx_packet_info_cmp
- );
- 
- static void
-@@ -75,7 +75,7 @@
- }
- 
- void
--lgtd_lifx_wire_load_packet_infos_map(void)
-+lgtd_lifx_wire_load_packet_info_map(void)
- {
- #define DECODER(x)  ((void (*)(void *))(x))
- #define ENCODER(x)  ((void (*)(void *))(x))
-@@ -91,7 +91,7 @@
-     .decode = lgtd_lifx_wire_null_packet_encoder_decoder,   \
-     .handle = lgtd_lifx_wire_null_packet_handler
- 
--    static struct lgtd_lifx_packet_infos packet_table[] = {
-+    static struct lgtd_lifx_packet_info packet_table[] = {
-         // Gateway packets:
-         {
-             REQUEST_ONLY,
-@@ -192,18 +192,18 @@
- 
-     for (int i = 0; i != LGTD_ARRAY_SIZE(packet_table); ++i) {
-         RB_INSERT(
--            lgtd_lifx_packet_infos_map,
--            &lgtd_lifx_packet_infos,
-+            lgtd_lifx_packet_info_map,
-+            &lgtd_lifx_packet_info,
-             &packet_table[i]
-         );
-     }
- }
- 
--const struct lgtd_lifx_packet_infos *
--lgtd_lifx_wire_get_packet_infos(enum lgtd_lifx_packet_type packet_type)
-+const struct lgtd_lifx_packet_info *
-+lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type packet_type)
- {
--    struct lgtd_lifx_packet_infos pkt_infos = { .type = packet_type };
--    return RB_FIND(lgtd_lifx_packet_infos_map, &lgtd_lifx_packet_infos, &pkt_infos);
-+    struct lgtd_lifx_packet_info pkt_info = { .type = packet_type };
-+    return RB_FIND(lgtd_lifx_packet_info_map, &lgtd_lifx_packet_info, &pkt_info);
- }
- 
- 
-@@ -273,7 +273,7 @@
-     hdr->packet_type = le16toh(hdr->packet_type);
- }
- 
--const struct lgtd_lifx_packet_infos *
-+const struct lgtd_lifx_packet_info *
- lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *hdr,
-                             enum lgtd_lifx_target_type target_type,
-                             union lgtd_lifx_target target,
-@@ -282,13 +282,13 @@
- {
-     assert(hdr);
- 
--    const struct lgtd_lifx_packet_infos *pkt_infos =
--        lgtd_lifx_wire_get_packet_infos(packet_type);
-+    const struct lgtd_lifx_packet_info *pkt_info =
-+        lgtd_lifx_wire_get_packet_info(packet_type);
- 
--    assert(pkt_infos);
-+    assert(pkt_info);
- 
-     memset(hdr, 0, sizeof(*hdr));
--    hdr->size = pkt_infos->size + sizeof(*hdr);
-+    hdr->size = pkt_info->size + sizeof(*hdr);
-     hdr->packet_type = packet_type;
-     if (site) {
-         memcpy(hdr->site, site, sizeof(hdr->site));
-@@ -313,7 +313,7 @@
- 
-     lgtd_lifx_wire_encode_header(hdr, flags);
- 
--    return pkt_infos;
-+    return pkt_info;
- }
- 
- void
-diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
---- a/lifx/wire_proto.h
-+++ b/lifx/wire_proto.h
-@@ -265,8 +265,8 @@
- 
- struct lgtd_lifx_gateway;
- 
--struct lgtd_lifx_packet_infos {
--    RB_ENTRY(lgtd_lifx_packet_infos)    link;
-+struct lgtd_lifx_packet_info {
-+    RB_ENTRY(lgtd_lifx_packet_info)     link;
-     const char                          *name;
-     enum lgtd_lifx_packet_type          type;
-     unsigned                            size;
-@@ -276,11 +276,11 @@
-                                                   const struct lgtd_lifx_packet_header *,
-                                                   const void *);
- };
--RB_HEAD(lgtd_lifx_packet_infos_map, lgtd_lifx_packet_infos);
-+RB_HEAD(lgtd_lifx_packet_info_map, lgtd_lifx_packet_info);
- 
- static inline int
--lgtd_lifx_packet_infos_cmp(struct lgtd_lifx_packet_infos *a,
--                           struct lgtd_lifx_packet_infos *b)
-+lgtd_lifx_packet_info_cmp(struct lgtd_lifx_packet_info *a,
-+                           struct lgtd_lifx_packet_info *b)
- {
-     return a->type - b->type;
- }
-@@ -331,10 +331,10 @@
- 
- enum lgtd_lifx_waveform_type lgtd_lifx_wire_waveform_string_id_to_type(const char *, int);
- 
--const struct lgtd_lifx_packet_infos *lgtd_lifx_wire_get_packet_infos(enum lgtd_lifx_packet_type);
--void lgtd_lifx_wire_load_packet_infos_map(void);
-+const struct lgtd_lifx_packet_info *lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type);
-+void lgtd_lifx_wire_load_packet_info_map(void);
- 
--const struct lgtd_lifx_packet_infos *lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *,
-+const struct lgtd_lifx_packet_info *lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *,
-                                                                  enum lgtd_lifx_target_type,
-                                                                  union lgtd_lifx_target,
-                                                                  const uint8_t *,
-diff --git a/tests/core/proto/test_proto_tag_create.c b/tests/core/proto/test_proto_tag_create.c
---- a/tests/core/proto/test_proto_tag_create.c
-+++ b/tests/core/proto/test_proto_tag_create.c
-@@ -65,7 +65,7 @@
- bool
- lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-                                enum lgtd_lifx_packet_type pkt_type,
--                               const void *pkt)
-+                               void *pkt)
- {
-     if (!gw) {
-         errx(1, "missing gateway");
-diff --git a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
---- a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
-+++ b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
-@@ -61,7 +61,7 @@
- bool
- lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-                                enum lgtd_lifx_packet_type pkt_type,
--                               const void *pkt)
-+                               void *pkt)
- {
-     (void)gw;
-     (void)pkt_type;
-diff --git a/tests/core/proto/test_proto_tag_update.c b/tests/core/proto/test_proto_tag_update.c
---- a/tests/core/proto/test_proto_tag_update.c
-+++ b/tests/core/proto/test_proto_tag_update.c
-@@ -69,7 +69,7 @@
- bool
- lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-                                enum lgtd_lifx_packet_type pkt_type,
--                               const void *pkt)
-+                               void *pkt)
- {
-     if (!gw) {
-         errx(1, "missing gateway");
-diff --git a/tests/core/router/test_router_send_to_broadcast.c b/tests/core/router/test_router_send_to_broadcast.c
---- a/tests/core/router/test_router_send_to_broadcast.c
-+++ b/tests/core/router/test_router_send_to_broadcast.c
-@@ -8,7 +8,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     lgtd_tests_insert_mock_gateway(2);
-     lgtd_tests_insert_mock_gateway(1);
-diff --git a/tests/core/router/test_router_send_to_device.c b/tests/core/router/test_router_send_to_device.c
---- a/tests/core/router/test_router_send_to_device.c
-+++ b/tests/core/router/test_router_send_to_device.c
-@@ -7,7 +7,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1);
-     struct lgtd_lifx_bulb *bulb_1 = lgtd_tests_insert_mock_bulb(gw_1, 1);
-diff --git a/tests/core/router/test_router_send_to_invalid_targets.c b/tests/core/router/test_router_send_to_invalid_targets.c
---- a/tests/core/router/test_router_send_to_invalid_targets.c
-+++ b/tests/core/router/test_router_send_to_invalid_targets.c
-@@ -31,7 +31,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1);
-     lgtd_tests_insert_mock_bulb(gw_1, 1);
-diff --git a/tests/core/router/test_router_send_to_label.c b/tests/core/router/test_router_send_to_label.c
---- a/tests/core/router/test_router_send_to_label.c
-+++ b/tests/core/router/test_router_send_to_label.c
-@@ -7,7 +7,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1);
-     struct lgtd_lifx_bulb *bulb_1 = lgtd_tests_insert_mock_bulb(gw_1, 1);
-diff --git a/tests/core/router/test_router_send_to_tag.c b/tests/core/router/test_router_send_to_tag.c
---- a/tests/core/router/test_router_send_to_tag.c
-+++ b/tests/core/router/test_router_send_to_tag.c
-@@ -7,7 +7,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1);
-     struct lgtd_lifx_gateway *gw_2 = lgtd_tests_insert_mock_gateway(2);
-diff --git a/tests/core/router/test_router_targets_to_devices.c b/tests/core/router/test_router_targets_to_devices.c
---- a/tests/core/router/test_router_targets_to_devices.c
-+++ b/tests/core/router/test_router_targets_to_devices.c
-@@ -41,7 +41,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway *gw_1 = lgtd_tests_insert_mock_gateway(1);
-     struct lgtd_lifx_gateway *gw_2 = lgtd_tests_insert_mock_gateway(2);
-diff --git a/tests/core/router/tests_router_utils.h b/tests/core/router/tests_router_utils.h
---- a/tests/core/router/tests_router_utils.h
-+++ b/tests/core/router/tests_router_utils.h
-@@ -13,12 +13,9 @@
- void
- lgtd_lifx_gateway_enqueue_packet(struct lgtd_lifx_gateway *gw,
-                                  const struct lgtd_lifx_packet_header *hdr,
--                                 enum lgtd_lifx_packet_type pkt_type,
--                                 const void *pkt,
--                                 int pkt_size)
-+                                 const struct lgtd_lifx_packet_info *pkt_info,
-+                                 void *pkt)
- {
--    (void)pkt_type;
--
-     lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].gw = gw;
-     // headers are created on the stack so we need to dup them:
-     lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].hdr = malloc(
-@@ -30,7 +27,7 @@
-         sizeof(*hdr)
-     );
-     lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].pkt = pkt;
--    lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].pkt_size = pkt_size;
-+    lgtd_tests_gw_pkt_queue[lgtd_tests_gw_pkt_queue_size].pkt_size = pkt_info->size;
-     lgtd_tests_gw_pkt_queue_size++;
- }
- 
-diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id.c b/tests/lifx/gateway/test_gateway_allocate_tag_id.c
---- a/tests/lifx/gateway/test_gateway_allocate_tag_id.c
-+++ b/tests/lifx/gateway/test_gateway_allocate_tag_id.c
-@@ -40,7 +40,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id_from_lifx_network.c b/tests/lifx/gateway/test_gateway_allocate_tag_id_from_lifx_network.c
---- a/tests/lifx/gateway/test_gateway_allocate_tag_id_from_lifx_network.c
-+++ b/tests/lifx/gateway/test_gateway_allocate_tag_id_from_lifx_network.c
-@@ -41,7 +41,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c
---- a/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c
-+++ b/tests/lifx/gateway/test_gateway_allocate_tag_id_no_tag_id_left.c
-@@ -41,7 +41,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_deallocate_tag_id_from_lifx_network.c b/tests/lifx/gateway/test_gateway_deallocate_tag_id_from_lifx_network.c
---- a/tests/lifx/gateway/test_gateway_deallocate_tag_id_from_lifx_network.c
-+++ b/tests/lifx/gateway/test_gateway_deallocate_tag_id_from_lifx_network.c
-@@ -23,7 +23,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_enqueue_packet.c b/tests/lifx/gateway/test_gateway_enqueue_packet.c
---- a/tests/lifx/gateway/test_gateway_enqueue_packet.c
-+++ b/tests/lifx/gateway/test_gateway_enqueue_packet.c
-@@ -5,7 +5,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-@@ -17,7 +17,7 @@
-     union lgtd_lifx_target target = { .tags = 0 };
- 
-     struct lgtd_lifx_packet_header hdr;
--    lgtd_lifx_wire_setup_header(
-+    const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header(
-         &hdr,
-         LGTD_LIFX_TARGET_ALL_DEVICES,
-         target,
-@@ -25,9 +25,7 @@
-         LGTD_LIFX_SET_POWER_STATE
-     );
- 
--    lgtd_lifx_gateway_enqueue_packet(
--        &gw, &hdr, LGTD_LIFX_SET_POWER_STATE, &pkt, sizeof(pkt)
--    );
-+    lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt);
- 
-     if (memcmp(gw_write_buf, &hdr, sizeof(hdr))) {
-         errx(1, "header incorrectly buffered");
-diff --git a/tests/lifx/gateway/test_gateway_enqueue_packet_ring_full.c b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_full.c
---- a/tests/lifx/gateway/test_gateway_enqueue_packet_ring_full.c
-+++ b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_full.c
-@@ -5,7 +5,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-@@ -17,7 +17,7 @@
-     union lgtd_lifx_target target = { .tags = 0 };
- 
-     struct lgtd_lifx_packet_header hdr;
--    lgtd_lifx_wire_setup_header(
-+    const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header(
-         &hdr,
-         LGTD_LIFX_TARGET_ALL_DEVICES,
-         target,
-@@ -29,9 +29,7 @@
-     gw.pkt_ring_head = 1;
-     gw.pkt_ring_tail = 2;
- 
--    lgtd_lifx_gateway_enqueue_packet(
--        &gw, &hdr, LGTD_LIFX_SET_POWER_STATE, &pkt, sizeof(pkt)
--    );
-+    lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt);
- 
-     if (memcmp(gw_write_buf, &hdr, sizeof(hdr))) {
-         errx(1, "header incorrectly buffered");
-@@ -65,9 +63,7 @@
-         errx(1, "event_add should have been called with gw.write_ev");
-     }
- 
--    lgtd_lifx_gateway_enqueue_packet(
--        &gw, &hdr, LGTD_LIFX_SET_POWER_STATE, &pkt, sizeof(pkt)
--    );
-+    lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt);
- 
-     if (gw_write_buf_idx != sizeof(pkt) + sizeof(hdr)) {
-         errx(1, "nothing should have been buffered");
-diff --git a/tests/lifx/gateway/test_gateway_enqueue_packet_ring_wraparound.c b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_wraparound.c
---- a/tests/lifx/gateway/test_gateway_enqueue_packet_ring_wraparound.c
-+++ b/tests/lifx/gateway/test_gateway_enqueue_packet_ring_wraparound.c
-@@ -5,7 +5,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-@@ -17,7 +17,7 @@
-     union lgtd_lifx_target target = { .tags = 0 };
- 
-     struct lgtd_lifx_packet_header hdr;
--    lgtd_lifx_wire_setup_header(
-+    const struct lgtd_lifx_packet_info *pkt_info = lgtd_lifx_wire_setup_header(
-         &hdr,
-         LGTD_LIFX_TARGET_ALL_DEVICES,
-         target,
-@@ -31,9 +31,7 @@
-     gw.pkt_ring_head = pkt_ring_last_idx;
-     gw.pkt_ring_tail = 2;
- 
--    lgtd_lifx_gateway_enqueue_packet(
--        &gw, &hdr, LGTD_LIFX_SET_POWER_STATE, &pkt, sizeof(pkt)
--    );
-+    lgtd_lifx_gateway_enqueue_packet(&gw, &hdr, pkt_info, &pkt);
- 
-     if (memcmp(gw_write_buf, &hdr, sizeof(hdr))) {
-         errx(1, "header incorrectly buffered");
-diff --git a/tests/lifx/gateway/test_gateway_handle_tag_labels.c b/tests/lifx/gateway/test_gateway_handle_tag_labels.c
---- a/tests/lifx/gateway/test_gateway_handle_tag_labels.c
-+++ b/tests/lifx/gateway/test_gateway_handle_tag_labels.c
-@@ -7,7 +7,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_update_tag_refcounts.c b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c
---- a/tests/lifx/gateway/test_gateway_update_tag_refcounts.c
-+++ b/tests/lifx/gateway/test_gateway_update_tag_refcounts.c
-@@ -5,7 +5,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_write_callback.c b/tests/lifx/gateway/test_gateway_write_callback.c
---- a/tests/lifx/gateway/test_gateway_write_callback.c
-+++ b/tests/lifx/gateway/test_gateway_write_callback.c
-@@ -43,7 +43,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_write_callback_clears_ring_full_flag.c b/tests/lifx/gateway/test_gateway_write_callback_clears_ring_full_flag.c
---- a/tests/lifx/gateway/test_gateway_write_callback_clears_ring_full_flag.c
-+++ b/tests/lifx/gateway/test_gateway_write_callback_clears_ring_full_flag.c
-@@ -43,7 +43,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_write_callback_last_packet_on_ring.c b/tests/lifx/gateway/test_gateway_write_callback_last_packet_on_ring.c
---- a/tests/lifx/gateway/test_gateway_write_callback_last_packet_on_ring.c
-+++ b/tests/lifx/gateway/test_gateway_write_callback_last_packet_on_ring.c
-@@ -42,7 +42,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_write_callback_partial_write.c b/tests/lifx/gateway/test_gateway_write_callback_partial_write.c
---- a/tests/lifx/gateway/test_gateway_write_callback_partial_write.c
-+++ b/tests/lifx/gateway/test_gateway_write_callback_partial_write.c
-@@ -50,7 +50,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/gateway/test_gateway_write_callback_ring_wraparound.c b/tests/lifx/gateway/test_gateway_write_callback_ring_wraparound.c
---- a/tests/lifx/gateway/test_gateway_write_callback_ring_wraparound.c
-+++ b/tests/lifx/gateway/test_gateway_write_callback_ring_wraparound.c
-@@ -43,7 +43,7 @@
- int
- main(void)
- {
--    lgtd_lifx_wire_load_packet_infos_map();
-+    lgtd_lifx_wire_load_packet_info_map();
- 
-     struct lgtd_lifx_gateway gw;
-     memset(&gw, 0, sizeof(gw));
-diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h
---- a/tests/lifx/mock_gateway.h
-+++ b/tests/lifx/mock_gateway.h
-@@ -11,7 +11,7 @@
- bool
- lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-                                enum lgtd_lifx_packet_type pkt_type,
--                               const void *pkt)
-+                               void *pkt)
- {
-     (void)gw;
-     (void)pkt_type;
--- a/relax_timings.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-# HG changeset patch
-# Parent  fbfdbecbaa8d13e475e4851ccda9065c6369c437
-Relax different timings a bit (until we get auto-retry on everything)
-
-diff --git a/lifx/gateway.h b/lifx/gateway.h
---- a/lifx/gateway.h
-+++ b/lifx/gateway.h
-@@ -21,7 +21,7 @@
- // according to my own tests, aggressively polling a bulb doesn't raise its
- // consumption at all (and it's interesting to note that a turned off bulb
- // still draw about 2W in ZigBee and about 3W in WiFi).
--enum { LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 200 };
-+enum { LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS = 800 };
- 
- // You can't send more than one lifx packet per UDP datagram.
- enum { LGTD_LIFX_GATEWAY_PACKET_RING_SIZE = 16 };
-diff --git a/lifx/timer.h b/lifx/timer.h
---- a/lifx/timer.h
-+++ b/lifx/timer.h
-@@ -17,11 +17,11 @@
- 
- #pragma once
- 
--enum { LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 200 };
-+enum { LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 500 };
- enum { LGTD_LIFX_TIMER_ACTIVE_DISCOVERY_INTERVAL_MSECS = 2000 };
- enum { LGTD_LIFX_TIMER_PASSIVE_DISCOVERY_INTERVAL_MSECS = 10000 };
--enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 2000 };
--enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 600 };
-+enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 3000 };
-+enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 2000 };
- 
- bool lgtd_lifx_timer_setup(void);
- void lgtd_lifx_timer_close(void);
--- a/remove_assert_on_request_id.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# HG changeset patch
-# Parent  5b607d851a9c8d71384dd29984f629f18f3366ca
-Don't abort on request without an id in debug builds
-
-We made it optional.
-
-diff --git a/core/jsonrpc.c b/core/jsonrpc.c
---- a/core/jsonrpc.c
-+++ b/core/jsonrpc.c
-@@ -1118,7 +1118,6 @@
-     }
- 
-     assert(request.method);
--    assert(request.id);
- 
-     for (int i = 0; i != LGTD_ARRAY_SIZE(methods); i++) {
-         int parsed_method_namelen = LGTD_JSONRPC_TOKEN_LEN(request.method);
--- a/series	Sat Aug 08 02:35:14 2015 -0700
+++ b/series	Sat Aug 08 02:36:59 2015 -0700
@@ -1,15 +0,0 @@
-ignore_duplicate_listening_address.patch
-add_command_pipe.patch
-add_daemon_module.patch
-fix_usage_and_version.patch
-add_tag_and_untag.patch
-update_readme.patch
-fix_set_power_state.patch
-fix_crash_in_get_light_state_when_tag_does_not_exist.patch
-fix_lifx_wire_float_endian_functions_naming.patch
-ignore_pcaps.patch
-fix_unused_unused_attribute.patch
-pass_pkt_info_around.patch #-skip
-relax_timings.patch
-remove_assert_on_request_id.patch
-fix_targeting_on_big_endian_architectures.patch
--- a/update_readme.patch	Sat Aug 08 02:35:14 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-# HG changeset patch
-# Parent  f4870e0a3ea73f505d404cb84b496614a3db2af2
-Reword the README a bit
-
-diff --git a/README.rst b/README.rst
---- a/README.rst
-+++ b/README.rst
-@@ -10,8 +10,8 @@
- 
- - no discovery delay ever, you get all the bulbs and their state right away;
- - lightsd is always in sync with the bulbs and always know their state;
--- lightsd act as an abstraction layer and can expose new discovery mechanism and
--  totally new APIs;
-+- lightsd act as an abstraction layer and can expose new discovery mechanisms and
-+  an unified API across different kind of smart bulbs;
- - For those of you with a high paranoia factor, lightsd let you place your bulbs
-   in a totally separate and closed network.
- 
-@@ -24,8 +24,8 @@
- lightsd discovers your LIFX bulbs, stays in sync with them and support the
- following commands through a JSON-RPC_ interface:
- 
--- power_off;
--- power_on;
-+- power_off (with auto-retry);
-+- power_on (with auto-retry);
- - set_light_from_hsbk;
- - set_waveform (change the light according to a function like SAW or SINE);
- - get_light_state;
-@@ -103,4 +103,22 @@
- 
- Use the ``-f`` option to run lightsd in the foreground.
- 
-+Known issues
-+------------
-+
-+The grouping (tagging) code of the LIFX White 800 is bugged: after a tagging
-+operation the LIFX White 800 keep saying it has no tags. Reboot the bulb to make
-+the tags appears.
-+
-+Power ON/OFF are the only commands with auto-retry, i.e: lightsd will keep
-+sending the command to the bulb until its state changes. This is not implemented
-+(yet) for ``set_light_from_hsbk``, ``set_waveform``, ``tag`` and ``untag``.
-+
-+While lighsd appears to be pretty stable, if you want to run lightsd in the
-+background, I recommend doing so in a processor supervisor (e.g: Supervisor_)
-+that can restart lightsd if it crashes. Otherwise, please feel free to report
-+crashes to me.
-+
-+.. _Supervisor: http://www.supervisord.org/
-+
- .. vim: set tw=80 spelllang=en spell: