changeset 217:d320d75c9ca1

start re-ordering patches
author Louis Opter <kalessin@kalessin.fr>
date Mon, 03 Aug 2015 01:18:55 -0700
parents 1314ac3aafd4
children 1bb9e30ce9ae
files add_command_pipe.patch add_daemon_module.patch add_daemonization_with_nice_proctitle.patch add_tag_and_untag.patch daemon_module.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_usage_and_version.patch ignore_duplicate_listening_address.patch ignore_duplicated_listening_addresses.patch leftovers.patch series tag_untag.patch update_readme.patch
diffstat 15 files changed, 5620 insertions(+), 6159 deletions(-) [+]
line wrap: on
line diff
--- a/add_command_pipe.patch	Sun Aug 02 19:02:55 2015 -0700
+++ b/add_command_pipe.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -1,20 +1,31 @@
 # HG changeset patch
-# Parent  710a684b58e97eddc7d7a12d85b17b823d087a82
-Add a command pipe to easily do actions from a shell
-
-NOTE: the command pipe is write only, responses aren't returned.
+# Parent  b346d618c2526ae5de9814cdd8db56fff77b224b
 
 diff --git a/CMakeLists.txt b/CMakeLists.txt
 --- a/CMakeLists.txt
 +++ b/CMakeLists.txt
-@@ -43,6 +43,8 @@
+@@ -42,6 +42,8 @@
+     "-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}"
+ )
+ 
+ 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 on IPv4/v6, over a command (named) pipe and
++Unix sockets (coming up).
+ 
+ lightsd can target single or multiple bulbs at once:
  
 diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
 --- a/core/CMakeLists.txt
@@ -26,7 +37,7 @@
 +    pipe.c
      proto.c
      router.c
-     stats.c
+ )
 diff --git a/core/client.c b/core/client.c
 --- a/core/client.c
 +++ b/core/client.c
@@ -105,15 +116,6 @@
 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) {
 @@ -480,7 +480,7 @@
  lgtd_jsonrpc_write_id(struct lgtd_client *client)
  {
@@ -192,7 +194,7 @@
 diff --git a/core/lightsd.c b/core/lightsd.c
 --- a/core/lightsd.c
 +++ b/core/lightsd.c
-@@ -52,6 +52,7 @@
+@@ -45,6 +45,7 @@
  #include "jsmn.h"
  #include "jsonrpc.h"
  #include "client.h"
@@ -200,7 +202,7 @@
  #include "listen.h"
  #include "lightsd.h"
  
-@@ -69,6 +70,7 @@
+@@ -60,6 +61,7 @@
  lgtd_cleanup(void)
  {
      lgtd_listen_close_all();
@@ -208,15 +210,15 @@
      lgtd_client_close_all();
      lgtd_lifx_timer_close();
      lgtd_lifx_broadcast_close();
-@@ -135,10 +137,17 @@
+@@ -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 [-l ...]]\n"
-+        "  [-c,--comand-pipe /command/fifo -[c ...]]\n"
++        "  [-l,--listen addr:port [+]]\n"
++        "  [-c,--comand-pipe /command/fifo [+]]\n"
 +        "  [-f,--foreground]\n"
 +        "  [-t,--no-timestamps]\n"
 +        "  [-h,--help]\n"
@@ -224,11 +226,8 @@
 +        "  [-v,--verbosity debug|info|warning|error]\n",
          progname
      );
-+    lgtd_cleanup();
      exit(0);
- }
- 
-@@ -198,6 +207,7 @@
+@@ -141,6 +149,7 @@
  
      static const struct option long_opts[] = {
          {"listen",          required_argument, NULL, 'l'},
@@ -236,7 +235,7 @@
          {"foreground",      no_argument,       NULL, 'f'},
          {"no-timestamps",   no_argument,       NULL, 't'},
          {"help",            no_argument,       NULL, 'h'},
-@@ -205,7 +215,7 @@
+@@ -148,7 +157,7 @@
          {"version",         no_argument,       NULL, 'V'},
          {NULL,              0,                 NULL, 0}
      };
@@ -245,20 +244,11 @@
  
      for (int rv = getopt_long(argc, argv, short_opts, long_opts, NULL);
           rv != -1;
-@@ -218,8 +228,7 @@
-                 lgtd_usage(argv[0]);
+@@ -164,6 +173,12 @@
+             if (!lgtd_listen_open(optarg, sep + 1)) {
+                 exit(1);
              }
-             strncat(binds, optarg, LGTD_MIN(
--                sizeof(binds) - strlen(binds) - 1,
--                strlen(optarg)
-+                sizeof(binds) - strlen(binds) - 1, strlen(optarg)
-             ));
-             strncat(binds, ", ", LGTD_MIN(
-                 sizeof(binds) - strlen(binds) - 1, 2
-@@ -230,6 +239,11 @@
-             }
-             *sep = ':';
-             break;
++            break;
 +        case 'c':
 +            if (!lgtd_command_pipe_open(optarg)) {
 +                exit(1);
@@ -271,7 +261,7 @@
 new file mode 100644
 --- /dev/null
 +++ b/core/pipe.c
-@@ -0,0 +1,247 @@
+@@ -0,0 +1,248 @@
 +// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
 +//
 +// This file is part of lighstd.
@@ -311,7 +301,7 @@
 +#include "pipe.h"
 +#include "lightsd.h"
 +
-+static struct lgtd_command_pipe_list lgtd_command_pipes =
++struct lgtd_command_pipe_list lgtd_command_pipes =
 +    SLIST_HEAD_INITIALIZER(&lgtd_command_pipes);
 +
 +static void
@@ -448,6 +438,7 @@
 +        return false;
 +    }
 +
++    lgtd_client_open_from_pipe(&pipe->client);
 +    pipe->path = path;
 +    pipe->fd = -1;
 +
@@ -523,7 +514,7 @@
 new file mode 100644
 --- /dev/null
 +++ b/core/pipe.h
-@@ -0,0 +1,31 @@
+@@ -0,0 +1,33 @@
 +// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
 +//
 +// This file is part of lighstd.
@@ -553,6 +544,8 @@
 +};
 +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
@@ -567,7 +560,7 @@
      struct lgtd_router_device *device;
      SLIST_FOREACH(device, devices, link) {
          struct lgtd_lifx_bulb *bulb = device->device;
-@@ -204,15 +204,15 @@
+@@ -204,7 +204,7 @@
              );
              continue;
          }
@@ -576,19 +569,8 @@
  
          bool comma = false;
          int tag_id;
-         LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, bulb->state.tags) {
-             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, "\"");
-+                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(
-@@ -225,11 +225,11 @@
-             }
+@@ -215,11 +215,11 @@
+             comma = true;
          }
  
 -        LGTD_CLIENT_WRITE_STRING(
@@ -620,120 +602,6 @@
 +{
 +    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
@@ -1952,10 +1820,102 @@
 +    (void)parsed;
 +}
 +#endif
+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
+@@ -76,7 +76,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_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);
+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
-@@ -18,7 +18,7 @@
+@@ -1,5 +1,7 @@
+ #pragma once
+ 
++#define FAKE_BUFFEREVENT (void *)0xfeed
++
+ void
+ lgtd_client_start_send_response(struct lgtd_client *client)
+ {
+@@ -16,7 +18,7 @@
  void
  lgtd_client_send_response(struct lgtd_client *client, const char *msg)
  {
@@ -1964,18 +1924,6 @@
  }
  #endif
  
-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
-@@ -24,7 +24,7 @@
-     .verbosity = LGTD_DEBUG
- };
- 
--struct event_base *lgtd_ev_base = NULL;
-+struct event_base *lgtd_ev_base = (void *)0x1234;
- 
- const char *lgtd_binds = NULL;
- 
 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
@@ -1999,7 +1947,7 @@
  #include <event2/util.h>
  
 @@ -112,3 +117,42 @@
- 
+     LIST_INSERT_HEAD(&tag->sites, site, link);
      return site;
  }
 +
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/add_daemon_module.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,1315 @@
+# HG changeset patch
+# Parent  98585a1c815188fb358356d2dc68c2b06bbf86b6
+
+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/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/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/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt
+--- a/tests/core/proto/CMakeLists.txt
++++ b/tests/core/proto/CMakeLists.txt
+@@ -3,17 +3,17 @@
+     ${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/timer.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_proto ${TIME_MONOTONIC_LIBRARY})
+ 
+ FUNCTION(ADD_ROUTER_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/timer.c
+     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
+@@ -14,11 +15,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,8 +34,8 @@
+ 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_listen_list lgtd_listeners =
++    SLIST_HEAD_INITIALIZER(&lgtd_listeners);
+ 
+ struct lgtd_lifx_gateway *
+ lgtd_tests_insert_mock_gateway(int id)
+@@ -44,6 +47,8 @@
+ 
+     LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link);
+ 
++    LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(gateways, 1);
++
+     return gw;
+ }
+ 
+@@ -118,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_daemonization_with_nice_proctitle.patch	Sun Aug 02 19:02:55 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,746 +0,0 @@
-# HG changeset patch
-# Parent  33ec84c490e97e4958a011ba16b113561be2aa19
-Add daemonization with a nice process title
-
-NOTE: the -f argument is now actually required to start lightsd in the
-      foreground.
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -22,10 +22,13 @@
- # 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 #########################################################
- 
-@@ -34,14 +37,17 @@
- 
- SET(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"
-+
-+    "-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")
-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/core/CMakeLists.txt b/core/CMakeLists.txt
---- a/core/CMakeLists.txt
-+++ b/core/CMakeLists.txt
-@@ -22,6 +22,7 @@
-     log.c
-     proto.c
-     router.c
-+    stats.c
- )
- 
- TARGET_LINK_LIBRARIES(
-@@ -30,3 +31,7 @@
-     ${EVENT2_CORE_LIBRARY}
-     ${TIME_MONOTONIC_LIBRARY}
- )
-+
-+IF (HAVE_LIBBSD)
-+    TARGET_LINK_LIBRARIES(lightsd ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
-diff --git a/core/lightsd.c b/core/lightsd.c
---- a/core/lightsd.c
-+++ b/core/lightsd.c
-@@ -17,11 +17,13 @@
- 
- #include <sys/queue.h>
- #include <sys/tree.h>
-+#include <sys/types.h>
- #include <arpa/inet.h>
- #include <assert.h>
- #include <endian.h>
- #include <err.h>
- #include <errno.h>
-+#include <fcntl.h>
- #include <getopt.h>
- #include <signal.h>
- #include <stdarg.h>
-@@ -31,6 +33,11 @@
- #include <stdlib.h>
- #include <string.h>
- #include <strings.h>
-+#include <unistd.h>
-+
-+#if LGTD_HAVE_LIBBSD
-+#include <bsd/bsd.h>
-+#endif
- 
- #include <event2/event.h>
- #include <event2/event_struct.h>
-@@ -56,6 +63,8 @@
- 
- struct event_base *lgtd_ev_base = NULL;
- 
-+const char *lgtd_binds;
-+
- void
- lgtd_cleanup(void)
- {
-@@ -133,9 +142,57 @@
-     exit(0);
- }
- 
-+static bool
-+lgtd_daemonize(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 FORK()  do {        \
-+    switch (fork()) {       \
-+        case 0:             \
-+            break;          \
-+        case -1:            \
-+            return false;   \
-+        default:            \
-+            exit(0);        \
-+    }                       \
-+} while (0)
-+
-+    FORK();
-+    setsid();
-+
-+    FORK();
-+
-+    return true;
-+}
-+
- int
--main(int argc, char *argv[])
-+main(int argc, char *argv[], char *envp[])
- {
-+    char binds[512] = { 0 };
-+    lgtd_binds = binds;
-+
-+#if LGTD_HAVE_LIBBSD
-+    setproctitle_init(argc, argv, envp);
-+#else
-+    (void)envp;
-+#endif
-     lgtd_configure_libevent();
-     lgtd_configure_signal_handling();
- 
-@@ -160,10 +217,19 @@
-             if (!sep || !sep[1]) {
-                 lgtd_usage(argv[0]);
-             }
-+            strncat(binds, optarg, LGTD_MIN(
-+                sizeof(binds) - strlen(binds) - 1,
-+                strlen(optarg)
-+            ));
-+            strncat(binds, ", ", LGTD_MIN(
-+                sizeof(binds) - strlen(binds) - 1, 2
-+            ));
-             *sep = '\0';
-             if (!lgtd_listen_open(optarg, sep + 1)) {
-                 exit(1);
-             }
-+            *sep = ':';
-+            break;
-         case 'f':
-             lgtd_opts.foreground = true;
-             break;
-@@ -194,6 +260,12 @@
-         }
-     }
- 
-+    binds[LGTD_MAX(strlen(binds) - 1, 0)] = '\0';
-+    binds[LGTD_MAX(strlen(binds) - 2, 0)] = '\0';
-+    if (binds[sizeof(binds) - 2]) {
-+        memset(binds + sizeof(binds) - 4, '.', 3);
-+    }
-+
-     argc -= optind;
-     argv += optind;
- 
-@@ -202,6 +274,10 @@
-         lgtd_err(1, "can't setup lightsd");
-     }
- 
-+    if (!lgtd_opts.foreground && !lgtd_daemonize()) {
-+        lgtd_err(1, "can't fork to the background");
-+    }
-+
-     lgtd_lifx_timer_start_discovery();
- 
-     event_base_dispatch(lgtd_ev_base);
-diff --git a/core/lightsd.h b/core/lightsd.h
---- a/core/lightsd.h
-+++ b/core/lightsd.h
-@@ -17,12 +17,15 @@
- 
- #pragma once
- 
-+struct sockaddr_storage;
-+
- #ifndef __attribute__
- # define __atttribute__(e)
- #endif
- 
- #define LGTD_ABS(v) ((v) >= 0 ? (v) : (v) * -1)
- #define LGTD_MIN(a, b) ((a) < (b) ? (a) : (b))
-+#define LGTD_MAX(a, b) ((a) > (b) ? (a) : (b))
- #define LGTD_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
- #define LGTD_MSECS_TO_TIMEVAL(v) { \
-     .tv_sec = (v) / 1000,           \
-@@ -44,6 +47,7 @@
-     enum lgtd_verbosity verbosity;
- };
- 
-+extern const char *lgtd_binds;
- extern struct lgtd_opts lgtd_opts;
- extern struct event_base *lgtd_ev_base;
- 
-@@ -62,5 +66,6 @@
- void lgtd_info(const char *, ...) __attribute__((format(printf, 1, 2)));
- void lgtd_debug(const char *, ...) __attribute__((format(printf, 1, 2)));
- void lgtd_libevent_log(int, const char *);
-+void lgtd_update_proctitle(void);
- 
- void lgtd_cleanup(void);
-diff --git a/core/log.c b/core/log.c
---- a/core/log.c
-+++ b/core/log.c
-@@ -17,6 +17,7 @@
- 
- #include <sys/tree.h>
- #include <sys/time.h>
-+#include <sys/types.h>
- #include <arpa/inet.h>
- #include <assert.h>
- #include <endian.h>
-@@ -26,11 +27,17 @@
- #include <stdbool.h>
- #include <stdint.h>
- #include <stdio.h>
-+#include <stdlib.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
-@@ -170,6 +177,20 @@
-     case EVENT_LOG_MSG:     lgtd_info("%s", msg);  break;
-     case EVENT_LOG_WARN:    lgtd_warnx("%s", msg)  break;
-     case EVENT_LOG_ERR:     lgtd_warnx("%s", msg); break;
--    default:                                        break;
-+    default:                                       break;
-     }
- }
-+
-+void
-+lgtd_update_proctitle(void)
-+{
-+#if LGTD_HAVE_PROCTITLE
-+    setproctitle(
-+        "listening_on(%s); lifx_gateways(found=%d); bulbs(found=%d, on=%d)",
-+        lgtd_binds,
-+        LGTD_STATS_GET(gateways),
-+        LGTD_STATS_GET(bulbs),
-+        LGTD_STATS_GET(bulbs_powered_on)
-+    );
-+#endif
-+}
-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,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_stats {
-+    int gateways;
-+    int bulbs;
-+    int bulbs_powered_on;
-+};
-+
-+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_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,7 @@
- #include "core/time_monotonic.h"
- #include "bulb.h"
- #include "gateway.h"
-+#include "core/stats.h"
- #include "core/lightsd.h"
- 
- struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table =
-@@ -62,6 +63,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 +76,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 +100,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 +115,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,7 @@
- #include "core/client.h"
- #include "core/proto.h"
- #include "core/router.h"
-+#include "core/stats.h"
- #include "core/lightsd.h"
- 
- struct lgtd_lifx_gateway_list lgtd_lifx_gateways =
-@@ -54,6 +55,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 +286,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/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -317,16 +317,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
-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
-@@ -7,12 +7,16 @@
-     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})
-+IF (HAVE_LIBBSD)
-+    TARGET_LINK_LIBRARIES(test_core_jsonrpc ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_JSONRPC_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_jsonrpc)
-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
-@@ -7,6 +7,7 @@
-     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/timer.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-@@ -14,6 +15,9 @@
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
- )
- TARGET_LINK_LIBRARIES(test_core_proto ${TIME_MONOTONIC_LIBRARY})
-+IF (HAVE_LIBBSD)
-+    TARGET_LINK_LIBRARIES(test_core_proto ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_ROUTER_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
-@@ -103,7 +103,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
-@@ -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/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt
---- a/tests/core/router/CMakeLists.txt
-+++ b/tests/core/router/CMakeLists.txt
-@@ -7,6 +7,7 @@
-     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/timer.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
-@@ -15,10 +16,11 @@
- )
- 
- TARGET_LINK_LIBRARIES(
--    test_core_router
--    ${EVENT2_CORE_LIBRARY}
--    ${TIME_MONOTONIC_LIBRARY}
-+    test_core_router ${EVENT2_CORE_LIBRARY} ${TIME_MONOTONIC_LIBRARY}
- )
-+IF (HAVE_LIBBSD)
-+    TARGET_LINK_LIBRARIES(test_core_router ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_ROUTER_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_router)
-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
-@@ -26,6 +26,8 @@
- 
- struct event_base *lgtd_ev_base = NULL;
- 
-+const char *lgtd_binds = NULL;
-+
- void
- lgtd_cleanup(void)
- {
-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
-@@ -8,6 +8,7 @@
-     ${LIGHTSD_SOURCE_DIR}/core/log.c
-     ${LIGHTSD_SOURCE_DIR}/core/proto.c
-     ${LIGHTSD_SOURCE_DIR}/core/router.c
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/broadcast.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/bulb.c
-     ${LIGHTSD_SOURCE_DIR}/lifx/timer.c
-@@ -15,6 +16,9 @@
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
- )
- TARGET_LINK_LIBRARIES(test_lifx_gateway ${TIME_MONOTONIC_LIBRARY})
-+IF (HAVE_LIBBSD)
-+    TARGET_LINK_LIBRARIES(test_lifx_gateway ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_GATEWAY_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_gateway)
-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
-@@ -15,6 +15,8 @@
- 
- struct event_base *lgtd_ev_base = NULL;
- 
-+const char *lgtd_binds = NULL;
-+
- void
- lgtd_cleanup(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
-@@ -6,8 +6,12 @@
- ADD_LIBRARY(
-     test_lifx_wire_proto 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_wire_proto ${LIBBSD_LIBRARY})
-+ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/add_tag_and_untag.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,3738 @@
+# HG changeset patch
+# Parent  e6a5c70e43bf38915cfe7bf4614b7aa71be843e9
+
+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 on IPv4/v6, over a command (named) pipe and
+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/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)
+@@ -151,36 +168,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 +429,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 +579,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 +667,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 +681,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_build_target_list.c b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c
+--- a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c
++++ b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ static void
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_POWER_OFF
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_POWER_OFF
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_POWER_ON
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_POWER_ON
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_SET_WAVEFORM
+ #include "test_jsonrpc_utils.h"
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c
+--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c
++++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ 
+ #define LGTD_TESTING_SET_WAVEFORM
+ #include "test_jsonrpc_utils.h"
+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_extract_request_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c
+--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c
++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c
+--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c
++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c
+--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c
++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c
+--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c
++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c
+--- a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c
++++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_send_error.c b/tests/core/jsonrpc/test_jsonrpc_send_error.c
+--- a/tests/core/jsonrpc/test_jsonrpc_send_error.c
++++ b/tests/core/jsonrpc/test_jsonrpc_send_error.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_send_response.c b/tests/core/jsonrpc/test_jsonrpc_send_response.c
+--- a/tests/core/jsonrpc/test_jsonrpc_send_response.c
++++ b/tests/core/jsonrpc/test_jsonrpc_send_response.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ static void
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ static void
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ static void
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ static void
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer.c b/tests/core/jsonrpc/test_jsonrpc_type_integer.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_integer.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c
+--- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c
++++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c
+@@ -1,6 +1,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+diff --git a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c
+--- a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c
++++ b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c
+@@ -3,6 +3,7 @@
+ #include "jsonrpc.c"
+ 
+ #include "mock_client_buf.h"
++#include "mock_gateway.h"
+ #include "test_jsonrpc_utils.h"
+ 
+ int
+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
+@@ -95,3 +95,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/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/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt
+--- a/tests/core/proto/CMakeLists.txt
++++ b/tests/core/proto/CMakeLists.txt
+@@ -9,6 +9,7 @@
+     ${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
+     ${LIGHTSD_SOURCE_DIR}/lifx/wire_proto.c
+     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
+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
+@@ -2,6 +2,7 @@
+ 
+ #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_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
+@@ -2,6 +2,7 @@
+ 
+ #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_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
+@@ -2,6 +2,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.h"
+ #include "tests_utils.h"
+ 
+ #define MOCKED_CLIENT_SEND_RESPONSE
+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
+@@ -2,6 +2,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.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
+@@ -2,6 +2,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.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
+@@ -2,6 +2,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.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
+@@ -4,6 +4,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.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
+@@ -4,6 +4,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.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
+@@ -4,6 +4,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.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
+@@ -4,6 +4,7 @@
+ 
+ #include "mock_client_buf.h"
+ #include "mock_daemon.h"
++#include "mock_gateway.h"
+ #include "tests_utils.h"
+ 
+ #define MOCKED_CLIENT_SEND_RESPONSE
+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
+@@ -34,6 +34,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/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt
+--- a/tests/core/router/CMakeLists.txt
++++ b/tests/core/router/CMakeLists.txt
+@@ -9,6 +9,7 @@
+     ${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
+     ${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_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)",
++            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
+new file mode 100644
+--- /dev/null
++++ b/tests/lifx/mock_gateway.h
+@@ -0,0 +1,131 @@
++#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_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)
++{
++    (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
+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;
++}
+diff --git a/tests/lightsc b/tests/lightsc
+--- a/tests/lightsc
++++ b/tests/lightsc
+@@ -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
--- a/daemon_module.patch	Sun Aug 02 19:02:55 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1071 +0,0 @@
-# HG changeset patch
-# Parent  d7a2d37c150198e9b0626332d1cc57c538bd5447
-
-diff --git a/README.rst b/README.rst
---- a/README.rst
-+++ b/README.rst
-@@ -32,8 +32,8 @@
- - tag/untag (group/ungroup bulbs together);
- - 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 on IPv4/v6, over a command (named) pipe and
-+Unix sockets (coming up).
- 
- 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
-@@ -15,6 +15,7 @@
- ADD_EXECUTABLE(
-     lightsd
-     client.c
-+    daemon.c
-     jsmn.c
-     jsonrpc.c
-     listen.c
-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
-@@ -17,13 +17,11 @@
- 
- #include <sys/queue.h>
- #include <sys/tree.h>
--#include <sys/types.h>
- #include <arpa/inet.h>
- #include <assert.h>
- #include <endian.h>
- #include <err.h>
- #include <errno.h>
--#include <fcntl.h>
- #include <getopt.h>
- #include <signal.h>
- #include <stdarg.h>
-@@ -33,11 +31,6 @@
- #include <stdlib.h>
- #include <string.h>
- #include <strings.h>
--#include <unistd.h>
--
--#if LGTD_HAVE_LIBBSD
--#include <bsd/bsd.h>
--#endif
- 
- #include <event2/event.h>
- #include <event2/event_struct.h>
-@@ -54,6 +47,7 @@
- #include "client.h"
- #include "pipe.h"
- #include "listen.h"
-+#include "daemon.h"
- #include "lightsd.h"
- 
- struct lgtd_opts lgtd_opts = {
-@@ -64,8 +58,6 @@
- 
- struct event_base *lgtd_ev_base = NULL;
- 
--const char *lgtd_binds;
--
- void
- lgtd_cleanup(void)
- {
-@@ -151,57 +143,11 @@
-     exit(0);
- }
- 
--static bool
--lgtd_daemonize(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 FORK()  do {        \
--    switch (fork()) {       \
--        case 0:             \
--            break;          \
--        case -1:            \
--            return false;   \
--        default:            \
--            exit(0);        \
--    }                       \
--} while (0)
--
--    FORK();
--    setsid();
--
--    FORK();
--
--    return true;
--}
--
- int
- main(int argc, char *argv[], char *envp[])
- {
--    char binds[512] = { 0 };
--    lgtd_binds = binds;
-+    lgtd_daemon_setup_proctitle(argc, argv, envp);
- 
--#if LGTD_HAVE_LIBBSD
--    setproctitle_init(argc, argv, envp);
--#else
--    (void)envp;
--#endif
-     lgtd_configure_libevent();
-     lgtd_configure_signal_handling();
- 
-@@ -231,17 +177,10 @@
-             if (!sep || !sep[1]) {
-                 lgtd_usage(argv[0]);
-             }
--            strncat(binds, optarg, LGTD_MIN(
--                sizeof(binds) - strlen(binds) - 1, strlen(optarg)
--            ));
--            strncat(binds, ", ", LGTD_MIN(
--                sizeof(binds) - strlen(binds) - 1, 2
--            ));
-             *sep = '\0';
-             if (!lgtd_listen_open(optarg, sep + 1)) {
-                 exit(1);
-             }
--            *sep = ':';
-             break;
-         case 'c':
-             if (!lgtd_command_pipe_open(optarg)) {
-@@ -278,12 +217,6 @@
-         }
-     }
- 
--    binds[LGTD_MAX(strlen(binds) - 1, 0)] = '\0';
--    binds[LGTD_MAX(strlen(binds) - 2, 0)] = '\0';
--    if (binds[sizeof(binds) - 2]) {
--        memset(binds + sizeof(binds) - 4, '.', 3);
--    }
--
-     argc -= optind;
-     argv += optind;
- 
-@@ -292,7 +225,7 @@
-         lgtd_err(1, "can't setup lightsd");
-     }
- 
--    if (!lgtd_opts.foreground && !lgtd_daemonize()) {
-+    if (!lgtd_opts.foreground && !lgtd_daemon_unleash()) {
-         lgtd_err(1, "can't fork to the background");
-     }
- 
-diff --git a/core/lightsd.h b/core/lightsd.h
---- a/core/lightsd.h
-+++ b/core/lightsd.h
-@@ -47,7 +47,6 @@
-     enum lgtd_verbosity verbosity;
- };
- 
--extern const char *lgtd_binds;
- extern struct lgtd_opts lgtd_opts;
- extern struct event_base *lgtd_ev_base;
- 
-@@ -66,6 +65,5 @@
- void lgtd_info(const char *, ...) __attribute__((format(printf, 1, 2)));
- void lgtd_debug(const char *, ...) __attribute__((format(printf, 1, 2)));
- void lgtd_libevent_log(int, const char *);
--void lgtd_update_proctitle(void);
- 
- void lgtd_cleanup(void);
-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
-@@ -180,17 +180,3 @@
-     default:                                       break;
-     }
- }
--
--void
--lgtd_update_proctitle(void)
--{
--#if LGTD_HAVE_PROCTITLE
--    setproctitle(
--        "listening_on(%s); lifx_gateways(found=%d); bulbs(found=%d, on=%d)",
--        lgtd_binds,
--        LGTD_STATS_GET(gateways),
--        LGTD_STATS_GET(bulbs),
--        LGTD_STATS_GET(bulbs_powered_on)
--    );
--#endif
--}
-diff --git a/core/pipe.c b/core/pipe.c
---- a/core/pipe.c
-+++ b/core/pipe.c
-@@ -37,7 +37,7 @@
- #include "pipe.h"
- #include "lightsd.h"
- 
--static struct lgtd_command_pipe_list lgtd_command_pipes =
-+struct lgtd_command_pipe_list lgtd_command_pipes =
-     SLIST_HEAD_INITIALIZER(&lgtd_command_pipes);
- 
- static void
-@@ -174,6 +174,7 @@
-         return false;
-     }
- 
-+    lgtd_client_open_from_pipe(&pipe->client);
-     pipe->path = path;
-     pipe->fd = -1;
- 
-diff --git a/core/pipe.h b/core/pipe.h
---- a/core/pipe.h
-+++ b/core/pipe.h
-@@ -27,5 +27,7 @@
- };
- 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/stats.h b/core/stats.h
---- a/core/stats.h
-+++ b/core/stats.h
-@@ -21,13 +21,15 @@
-     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_update_proctitle();                                    \
-+
-+#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,7 @@
- #include "core/time_monotonic.h"
- #include "bulb.h"
- #include "gateway.h"
-+#include "core/daemon.h"
- #include "core/stats.h"
- #include "core/lightsd.h"
- 
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -45,6 +45,7 @@
- #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 =
-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/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/proto/CMakeLists.txt b/tests/core/proto/CMakeLists.txt
---- a/tests/core/proto/CMakeLists.txt
-+++ b/tests/core/proto/CMakeLists.txt
-@@ -16,11 +16,11 @@
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
- )
- 
--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
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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 "mock_gateway.h"
- #include "tests_utils.h"
- 
-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
-@@ -1,6 +1,7 @@
- #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"
-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
-@@ -1,6 +1,7 @@
- #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"
-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
-@@ -1,6 +1,7 @@
- #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"
-diff --git a/tests/core/proto/test_proto_untag.c b/tests/core/proto/test_proto_untag.c
---- a/tests/core/proto/test_proto_untag.c
-+++ b/tests/core/proto/test_proto_untag.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "mock_gateway.h"
- #include "tests_utils.h"
- 
-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
---- a/tests/core/proto/test_proto_untag_tag_does_not_exist.c
-+++ b/tests/core/proto/test_proto_untag_tag_does_not_exist.c
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_daemon.h"
- #include "mock_gateway.h"
- #include "tests_utils.h"
- 
-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,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_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_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
-@@ -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;
- }
- 
-@@ -118,6 +126,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/tests_shims.c b/tests/lifx/tests_shims.c
---- a/tests/lifx/tests_shims.c
-+++ b/tests/lifx/tests_shims.c
-@@ -37,3 +37,8 @@
-         return ntohs(in6_peer->sin6_port);
-     }
- }
-+
-+void
-+lgtd_daemon_update_proctitle(void)
-+{
-+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fix_crash_in_get_light_state_when_tag_does_not_exist.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,31 @@
+# HG changeset patch
+# Parent  53f4cb08f582e3657c896138823cf08ac2dcccf8
+
+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(
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fix_lifx_wire_float_endian_functions_naming.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,59 @@
+# HG changeset patch
+# Parent  80fb374d7109e03c44f049a2d7a845abf00663e1
+
+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
+@@ -37,7 +37,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
+@@ -37,7 +37,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) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fix_set_power_state.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,32 @@
+# HG changeset patch
+# Parent  46aa876b47a3e3c07b4855aa4cbff94acba75e16
+
+diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
+--- a/lifx/wire_proto.c
++++ b/lifx/wire_proto.c
+@@ -341,16 +341,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_usage_and_version.patch	Sun Aug 02 19:02:55 2015 -0700
+++ b/fix_usage_and_version.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -1,22 +1,18 @@
 # HG changeset patch
-# Parent  16c122197bf6ffffef579622c276b66b89e197b3
-Display the usage when no arguments are passed in and fix -V
+# Parent  298fe56d98508afd0b2d3098264f4661d5908fe5
 
 diff --git a/core/lightsd.c b/core/lightsd.c
 --- a/core/lightsd.c
 +++ b/core/lightsd.c
-@@ -138,8 +138,8 @@
- {
-     printf(
-         "Usage: %s ...\n\n"
--        "  [-l,--listen addr:port [-l ...]]\n"
--        "  [-c,--comand-pipe /command/fifo -[c ...]]\n"
-+        "  [-l,--listen addr:port [+]]\n"
-+        "  [-c,--comand-pipe /command/fifo [+]]\n"
-         "  [-f,--foreground]\n"
-         "  [-t,--no-timestamps]\n"
-         "  [-h,--help]\n"
-@@ -217,6 +217,10 @@
+@@ -138,6 +138,7 @@
+         "  [-v,--verbosity debug|info|warning|error]\n",
+         progname
+     );
++    lgtd_cleanup();
+     exit(0);
+ }
+ 
+@@ -159,6 +160,10 @@
      };
      const char short_opts[] = "l:c:fthv:V";
  
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ignore_duplicate_listening_address.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,36 @@
+# HG changeset patch
+# Parent  d8e34f128c677442c5c047793207dbc1664b6c2e
+
+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_duplicated_listening_addresses.patch	Sun Aug 02 19:02:55 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-# HG changeset patch
-# Parent  9e1c55d17b4004d7b2841177c8aec6a37391090d
-Ignore duplicated listen addresses
-
-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;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/leftovers.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,227 @@
+# HG changeset patch
+# Parent  2be7ea678eaadacc07b4e7b04af720409b5d2fca
+
+diff --git a/README.rst b/README.rst
+--- a/README.rst
++++ b/README.rst
+@@ -32,8 +32,8 @@
+ - tag/untag (group/ungroup bulbs together);
+ - 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 and
+-Unix sockets (coming up).
++The JSON-RPC interface works on top of 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/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) {
+diff --git a/core/lightsd.h b/core/lightsd.h
+--- a/core/lightsd.h
++++ b/core/lightsd.h
+@@ -17,6 +17,8 @@
+ 
+ #pragma once
+ 
++struct sockaddr_storage;
++
+ #ifndef __attribute__
+ # define __atttribute__(e)
+ #endif
+diff --git a/core/log.c b/core/log.c
+--- a/core/log.c
++++ b/core/log.c
+@@ -17,6 +17,7 @@
+ 
+ #include <sys/tree.h>
+ #include <sys/time.h>
++#include <sys/types.h>
+ #include <arpa/inet.h>
+ #include <assert.h>
+ #include <endian.h>
+@@ -26,6 +27,7 @@
+ #include <stdbool.h>
+ #include <stdint.h>
+ #include <stdio.h>
++#include <stdlib.h>
+ #include <time.h>
+ 
+ #if LGTD_HAVE_LIBBSD
+@@ -175,6 +177,6 @@
+     case EVENT_LOG_MSG:     lgtd_info("%s", msg);  break;
+     case EVENT_LOG_WARN:    lgtd_warnx("%s", msg)  break;
+     case EVENT_LOG_ERR:     lgtd_warnx("%s", msg); break;
+-    default:                                        break;
++    default:                                       break;
+     }
+ }
+diff --git a/core/router.c b/core/router.c
+--- a/core/router.c
++++ b/core/router.c
+@@ -61,6 +61,8 @@
+         );
+         assert(pkt_infos);
+ 
++        pkt_infos->encode(pkt);
++
+         lgtd_lifx_gateway_enqueue_packet(
+             gw, &hdr, pkt_type, pkt, pkt_infos->size
+         );
+@@ -372,8 +374,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/lifx/gateway.c b/lifx/gateway.c
+--- a/lifx/gateway.c
++++ b/lifx/gateway.c
+@@ -150,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;
+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 = 1000 };
+ 
+ // 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 = 4000 };
++enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 2000 };
+ 
+ bool lgtd_lifx_timer_setup(void);
+ void lgtd_lifx_timer_close(void);
+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
+@@ -16,11 +16,11 @@
+     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
+ )
+ 
+-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
+@@ -31,6 +31,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 = {
+@@ -41,7 +44,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);
+@@ -105,7 +109,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
+@@ -47,7 +47,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/tests_shims.c b/tests/core/tests_shims.c
+--- a/tests/core/tests_shims.c
++++ b/tests/core/tests_shims.c
+@@ -24,7 +24,9 @@
+     .verbosity = LGTD_DEBUG
+ };
+ 
+-struct event_base *lgtd_ev_base = NULL;
++struct event_base *lgtd_ev_base = (void *)0x1234;
++
++const char *lgtd_binds = NULL;
+ 
+ void
+ lgtd_cleanup(void)
+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
+@@ -15,6 +15,8 @@
+ 
+ struct event_base *lgtd_ev_base = NULL;
+ 
++const char *lgtd_binds = NULL;
++
+ void
+ lgtd_cleanup(void)
+ {
--- a/series	Sun Aug 02 19:02:55 2015 -0700
+++ b/series	Mon Aug 03 01:18:55 2015 -0700
@@ -1,6 +1,10 @@
-add_daemonization_with_nice_proctitle.patch
-tag_untag.patch
-ignore_duplicated_listening_addresses.patch
+ignore_duplicate_listening_address.patch
 add_command_pipe.patch
+add_daemon_module.patch
 fix_usage_and_version.patch
-daemon_module.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
+leftovers.patch
--- a/tag_untag.patch	Sun Aug 02 19:02:55 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4100 +0,0 @@
-# HG changeset patch
-# Parent  2f8c26d59f71c5c67b0252b2220df91d13e52321
-Add the ability to tag (group) or untag (ungroup) bulbs
-
-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)
- 
-@@ -35,7 +35,7 @@
- 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(
-     # Only relevant for the GNU libc:
-@@ -58,10 +58,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/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 on IPv4/v6, over a command (named) pipe
-@@ -104,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`` and ``set_waveform``.
-+
-+While lighsd appears to be pretty stable, if you want to run lightsd in the
-+background, I recommend doing it in a processor supervisor (e.g: Supervisor_)
-+that can restart lightsd in case of crash. Otherwise, please send me your crash
-+reports!
-+
-+.. _Supervisor: http://www.supervisord.org/
-+
- .. 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/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
-@@ -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(
-@@ -224,3 +234,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
-@@ -61,6 +61,8 @@
-         );
-         assert(pkt_infos);
- 
-+        pkt_infos->encode(pkt);
-+
-         lgtd_lifx_gateway_enqueue_packet(
-             gw, &hdr, pkt_type, pkt, pkt_infos->size
-         );
-@@ -372,8 +374,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/lifx/bulb.c b/lifx/bulb.c
---- a/lifx/bulb.c
-+++ b/lifx/bulb.c
-@@ -76,12 +76,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,
-@@ -107,6 +124,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));
- }
-@@ -124,3 +143,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
-@@ -70,9 +70,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(
-@@ -82,6 +82,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)
-@@ -132,13 +149,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;
-@@ -150,36 +160,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;
- }
-@@ -370,19 +421,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
-     );
- }
- 
-@@ -484,16 +571,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);
-@@ -544,9 +659,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;
-@@ -558,3 +673,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
-@@ -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 = 1000 };
- 
- // You can't send more than one lifx packet per UDP datagram.
- enum { LGTD_LIFX_GATEWAY_PACKET_RING_SIZE = 16 };
-@@ -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/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 = 4000 };
-+enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 2000 };
- 
- bool lgtd_lifx_timer_setup(void);
- void lgtd_lifx_timer_close(void);
-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)
-         }
-     };
- 
-@@ -357,11 +382,19 @@
-     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);
- }
- 
- 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);
-@@ -369,3 +402,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
-@@ -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;
-@@ -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/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/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/CMakeLists.txt b/tests/core/jsonrpc/CMakeLists.txt
---- a/tests/core/jsonrpc/CMakeLists.txt
-+++ b/tests/core/jsonrpc/CMakeLists.txt
-@@ -3,7 +3,7 @@
-     ${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
-@@ -13,10 +13,6 @@
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-     ${CMAKE_CURRENT_SOURCE_DIR}/../tests_utils.c
- )
--TARGET_LINK_LIBRARIES(test_core_jsonrpc ${TIME_MONOTONIC_LIBRARY})
--IF (HAVE_LIBBSD)
--    TARGET_LINK_LIBRARIES(test_core_jsonrpc ${LIBBSD_LIBRARY})
--ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_JSONRPC_TEST TEST_SOURCE)
-     ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_core_jsonrpc)
-diff --git a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c
---- a/tests/core/jsonrpc/test_jsonrpc_build_target_list.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_build_target_list.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- static void
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_POWER_OFF
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_POWER_OFF
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_POWER_ON
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_POWER_ON
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_SET_WAVEFORM
- #include "test_jsonrpc_utils.h"
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform_invalid_params.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- 
- #define LGTD_TESTING_SET_WAVEFORM
- #include "test_jsonrpc_utils.h"
-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_extract_request_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c
---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_no_params.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c
---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_notification_no_params.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c
---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_array.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c
---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_params_obj.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c
---- a/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_extract_request_valid_notification.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_send_error.c b/tests/core/jsonrpc/test_jsonrpc_send_error.c
---- a/tests/core/jsonrpc/test_jsonrpc_send_error.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_send_error.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_send_response.c b/tests/core/jsonrpc/test_jsonrpc_send_response.c
---- a/tests/core/jsonrpc/test_jsonrpc_send_response.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_send_response.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_invalid.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- static void
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_1_valid.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- static void
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_invalid.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- static void
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_float_between_0_and_360_valid.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- static void
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer.c b/tests/core/jsonrpc/test_jsonrpc_type_integer.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_integer.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_invalid_characters.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_big.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c
---- a/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_type_integer_too_small.c
-@@ -1,6 +1,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-diff --git a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c
---- a/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_uint16_range_to_float_string.c
-@@ -3,6 +3,7 @@
- #include "jsonrpc.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "test_jsonrpc_utils.h"
- 
- int
-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
-@@ -95,3 +95,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/CMakeLists.txt b/tests/core/proto/CMakeLists.txt
---- a/tests/core/proto/CMakeLists.txt
-+++ b/tests/core/proto/CMakeLists.txt
-@@ -3,21 +3,18 @@
-     ${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
-     ${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_proto ${TIME_MONOTONIC_LIBRARY})
--IF (HAVE_LIBBSD)
--    TARGET_LINK_LIBRARIES(test_core_proto ${LIBBSD_LIBRARY})
--ENDIF (HAVE_LIBBSD)
- 
- FUNCTION(ADD_ROUTER_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,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-@@ -29,17 +30,20 @@
-     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,
-+        .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);
-@@ -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);
-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_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-@@ -35,7 +36,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_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_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-@@ -45,7 +46,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
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.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")) {
-@@ -52,7 +53,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
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-@@ -52,7 +53,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
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-@@ -52,7 +53,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
-@@ -1,6 +1,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-@@ -52,7 +53,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_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_gateway.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_gateway.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_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-@@ -35,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
-@@ -3,6 +3,7 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#include "mock_gateway.h"
- #include "tests_utils.h"
- 
- #define MOCKED_CLIENT_SEND_RESPONSE
-@@ -35,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_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,252 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.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,208 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.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,282 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.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,169 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.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,89 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.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
-@@ -1,5 +1,7 @@
- #pragma once
- 
-+#define FAKE_BUFFEREVENT (void *)0xfeed
-+
- void
- lgtd_client_start_send_response(struct lgtd_client *client)
- {
-@@ -32,6 +34,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/router/CMakeLists.txt b/tests/core/router/CMakeLists.txt
---- a/tests/core/router/CMakeLists.txt
-+++ b/tests/core/router/CMakeLists.txt
-@@ -3,24 +3,20 @@
-     ${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
-     ${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_router ${EVENT2_CORE_LIBRARY} ${TIME_MONOTONIC_LIBRARY}
--)
--IF (HAVE_LIBBSD)
--    TARGET_LINK_LIBRARIES(test_core_router ${LIBBSD_LIBRARY})
--ENDIF (HAVE_LIBBSD)
-+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,7 @@
- #include "router.c"
- 
- #include "tests_utils.h"
-+
- #include "tests_router_utils.h"
- 
- int
-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
-@@ -32,52 +32,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
-@@ -26,9 +26,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)
- {
-@@ -108,7 +105,10 @@
-     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)",
-+            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/CMakeLists.txt b/tests/lifx/gateway/CMakeLists.txt
---- a/tests/lifx/gateway/CMakeLists.txt
-+++ b/tests/lifx/gateway/CMakeLists.txt
-@@ -3,25 +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})
--IF (HAVE_LIBBSD)
--    TARGET_LINK_LIBRARIES(test_lifx_gateway ${LIBBSD_LIBRARY})
--ENDIF (HAVE_LIBBSD)
- 
- 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/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
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/mock_gateway.h
-@@ -0,0 +1,131 @@
-+#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_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)
-+{
-+    (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
-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,18 +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
- )
--IF (HAVE_LIBBSD)
--    TARGET_LINK_LIBRARIES(test_lifx_wire_proto ${LIBBSD_LIBRARY})
--ENDIF (HAVE_LIBBSD)
- 
- 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")
-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;
-+}
-diff --git a/tests/lightsc b/tests/lightsc
---- a/tests/lightsc
-+++ b/tests/lightsc
-@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/update_readme.patch	Mon Aug 03 01:18:55 2015 -0700
@@ -0,0 +1,29 @@
+# HG changeset patch
+# Parent  04a46ad71a58fa6486754a8e97da9f06436d5a01
+
+diff --git a/README.rst b/README.rst
+--- a/README.rst
++++ b/README.rst
+@@ -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`` and ``set_waveform``.
++
++While lighsd appears to be pretty stable, if you want to run lightsd in the
++background, I recommend doing it in a processor supervisor (e.g: Supervisor_)
++that can restart lightsd in case of crash. Otherwise, please send me your crash
++reports!
++
++.. _Supervisor: http://www.supervisord.org/
++
+ .. vim: set tw=80 spelllang=en spell: