changeset 200:623a0664aecb

wip, merge pending tag wip into the first patch, still need more testing on the bulbs now
author Louis Opter <kalessin@kalessin.fr>
date Sun, 19 Jul 2015 21:27:29 -0700
parents 55031f07152b
children 36d245fddcdd
files fix_usage_and_version.patch series tag_untag.patch tag_untag_testing_wip.patch
diffstat 4 files changed, 1237 insertions(+), 1304 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fix_usage_and_version.patch	Sun Jul 19 21:27:29 2015 -0700
@@ -0,0 +1,30 @@
+# HG changeset patch
+# Parent  fd1b697fb0c8dd39053b9cdf1bb8b40f78c28436
+Display the usage when no arguments are passed in and fix -V
+
+diff --git a/core/lightsd.c b/core/lightsd.c
+--- a/core/lightsd.c
++++ b/core/lightsd.c
+@@ -217,6 +217,10 @@
+     };
+     const char short_opts[] = "l:c:fthv:V";
+ 
++    if (argc == 1) {
++        lgtd_usage(argv[0]);
++    }
++
+     for (int rv = getopt_long(argc, argv, short_opts, long_opts, NULL);
+          rv != -1;
+          rv = getopt_long(argc, argv, short_opts, long_opts, NULL)) {
+diff --git a/core/version.h.in b/core/version.h.in
+--- a/core/version.h.in
++++ b/core/version.h.in
+@@ -29,4 +29,7 @@
+ 
+ #pragma once
+ 
+-const char LGTD_VERSION[] = "@LGTD_VERSION@";
++const char LGTD_VERSION[] = (
++    "@LIGHTSD_VERSION@"
++    "\n\nCopyright (c) 2014, 2015, Louis Opter <kalessin@kalessin.fr>"
++);
--- a/series	Sun Jul 19 17:39:41 2015 -0700
+++ b/series	Sun Jul 19 21:27:29 2015 -0700
@@ -2,4 +2,4 @@
 tag_untag.patch
 ignore_duplicated_listening_addresses.patch
 add_command_pipe.patch
-tag_untag_testing_wip.patch
+fix_usage_and_version.patch
--- a/tag_untag.patch	Sun Jul 19 17:39:41 2015 -0700
+++ b/tag_untag.patch	Sun Jul 19 21:27:29 2015 -0700
@@ -1,5 +1,5 @@
 # HG changeset patch
-# Parent  d9cf460f9f1c472067fcbafb1a62b28d4842b709
+# Parent  40af4d3ae6619e5faeef92beecc2e0d876eaf91b
 Add the ability to tag (group) or untag (ungroup) bulbs
 
 diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -151,7 +151,7 @@
          }
  
          LGTD_CLIENT_WRITE_STRING(
-@@ -224,3 +234,138 @@
+@@ -224,3 +234,145 @@
  
      lgtd_router_device_list_free(devices);
  }
@@ -184,7 +184,7 @@
 +    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 builb
++    // 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;
@@ -200,12 +200,14 @@
 +    // 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 = lgtd_lifx_gateway_get_tag_id(site->gw, tag);
++        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, sizeof(pkt)
++            site->gw, LGTD_LIFX_SET_TAG_LABELS, &pkt
 +        );
 +        if (!enqueued) {
 +            goto error_site_alloc;
@@ -221,9 +223,13 @@
 +        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);
-+        struct lgtd_lifx_packet_tags pkt;
-+        pkt.tags = bulb->state.tags | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-+        lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt);
++        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);
@@ -281,6 +287,7 @@
 +            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);
 +            }
 +        }
@@ -302,7 +309,16 @@
 diff --git a/core/router.c b/core/router.c
 --- a/core/router.c
 +++ b/core/router.c
-@@ -372,8 +372,8 @@
+@@ -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)
  {
@@ -318,7 +334,15 @@
 diff --git a/lifx/bulb.c b/lifx/bulb.c
 --- a/lifx/bulb.c
 +++ b/lifx/bulb.c
-@@ -107,6 +107,8 @@
+@@ -81,7 +81,6 @@
+         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 +106,8 @@
          );
      }
  
@@ -327,7 +351,7 @@
      bulb->last_light_state_at = received_at;
      memcpy(&bulb->state, state, sizeof(bulb->state));
  }
-@@ -124,3 +126,13 @@
+@@ -124,3 +125,13 @@
  
      bulb->state.power = power;
  }
@@ -352,13 +376,131 @@
 diff --git a/lifx/gateway.c b/lifx/gateway.c
 --- a/lifx/gateway.c
 +++ b/lifx/gateway.c
-@@ -155,30 +155,11 @@
+@@ -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(gw, LGTD_LIFX_GET_LIGHT_STATE, NULL, 0);
++    lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_LIGHT_STATE, NULL);
  
 -    lgtd_lifx_wire_setup_header(
 -        &hdr,
@@ -381,39 +523,16 @@
 -    );
 -    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(
-+        gw, LGTD_LIFX_GET_TAG_LABELS, &pkt, sizeof(pkt)
-     );
++    lgtd_lifx_gateway_send_to_site_quiet(gw, LGTD_LIFX_GET_TAG_LABELS, &pkt);
  
      gw->pending_refresh_req = true;
-@@ -369,6 +350,68 @@
-     event_add(gw->write_ev, NULL);
+ }
+@@ -370,19 +421,55 @@
  }
  
-+bool
-+lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-+                               enum lgtd_lifx_packet_type pkt_type,
-+                               const void *pkt,
-+                               int pkt_size)
-+{
-+    assert(gw);
-+
-+    struct lgtd_lifx_packet_header hdr;
-+    union lgtd_lifx_target target = { .addr = gw->site.as_array };
-+    lgtd_lifx_wire_setup_header(
-+        &hdr,
-+        LGTD_LIFX_TARGET_SITE,
-+        target,
-+        gw->site.as_array,
-+        pkt_type
-+    );
-+    lgtd_lifx_gateway_enqueue_packet(gw, &hdr, pkt_type, pkt, pkt_size);
-+    
-+    return true; // FIXME, have real return values on the send paths...
-+}
-+
-+void
+ void
 +lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw,
 +                                       uint64_t bulb_tags,
 +                                       uint64_t pkt_tags)
@@ -441,22 +560,38 @@
 +            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, gw->site.as_array
++                gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array)
 +            );
 +            struct lgtd_lifx_packet_tag_labels pkt = {
-+                .tags = ~(gw->tag_ids & ~tag_id)
++                .tags = ~(gw->tag_ids & ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))
 +            };
-+            lgtd_lifx_gateway_send_to_site(
-+                gw, LGTD_LIFX_SET_TAG_LABELS, &pkt, sizeof(pkt)
-+            );
++            lgtd_lifx_wire_encode_tag_labels(&pkt);
++            lgtd_lifx_gateway_send_to_site(gw, LGTD_LIFX_SET_TAG_LABELS, &pkt);
 +        }
 +    }
 +}
 +
- void
++void
  lgtd_lifx_gateway_handle_pan_gateway(struct lgtd_lifx_gateway *gw,
                                       const struct lgtd_lifx_packet_header *hdr,
-@@ -484,16 +527,44 @@
+                                      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
@@ -503,7 +638,7 @@
      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 +615,9 @@
+@@ -544,9 +659,9 @@
      assert(gw && hdr && pkt);
  
      lgtd_debug(
@@ -515,7 +650,7 @@
      );
  
      int tag_id;
-@@ -558,3 +629,38 @@
+@@ -558,3 +673,38 @@
          }
      }
  }
@@ -574,15 +709,22 @@
      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
-@@ -92,7 +93,15 @@
+@@ -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 *,
-+                                    int);
++                                    const void *);
  
 +void lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *, uint64_t, uint64_t);
 +
@@ -691,11 +833,27 @@
 +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
-@@ -20,8 +20,8 @@
- enum { LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 200 };
+@@ -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 };
@@ -888,6 +1046,28 @@
 +void lgtd_lifx_wire_decode_tags(struct lgtd_lifx_packet_tags *);
 +void lgtd_lifx_wire_encode_tag_labels(struct lgtd_lifx_packet_tag_labels *);
  void lgtd_lifx_wire_decode_tag_labels(struct lgtd_lifx_packet_tag_labels *);
+diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_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 "tests_shims.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 "tests_shims.h"
+ 
+ #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
+ #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
@@ -1185,7 +1365,15 @@
 diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c
 --- a/tests/core/proto/test_proto_get_light_state.c
 +++ b/tests/core/proto/test_proto_get_light_state.c
-@@ -29,6 +29,9 @@
+@@ -1,6 +1,7 @@
+ #include "proto.c"
+ 
+ #include "mock_client_buf.h"
++#include "tests_shims.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);
  
@@ -1194,8 +1382,12 @@
 +    };
      static struct lgtd_lifx_bulb bulb_1 = {
          .addr = { 1, 2, 3, 4, 5 },
-         .state = {
-@@ -39,7 +42,8 @@
+-        .state = {
+-            .hue = 0xaaaa,
++        .state = { .hue = 0xaaaa,
+             .saturation = 0xffff,
+             .brightness = 0xbbbb,
+             .kelvin = 3600,
              .label = "wave",
              .power = LGTD_LIFX_POWER_ON,
              .tags = 0
@@ -1205,10 +1397,67 @@
      };
      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 "tests_shims.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 "tests_shims.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
-@@ -38,7 +38,7 @@
+@@ -1,6 +1,7 @@
+ #include "proto.c"
+ 
+ #include "mock_client_buf.h"
++#include "tests_shims.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) {
@@ -1217,10 +1466,109 @@
      }
  
      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 "tests_shims.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 "tests_shims.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 "tests_shims.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 "tests_shims.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 "tests_shims.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
-@@ -35,7 +35,7 @@
+@@ -3,6 +3,7 @@
+ #include "proto.c"
+ 
+ #include "mock_client_buf.h"
++#include "tests_shims.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);
@@ -1232,7 +1580,15 @@
 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
-@@ -35,7 +35,7 @@
+@@ -3,6 +3,7 @@
+ #include "proto.c"
+ 
+ #include "mock_client_buf.h"
++#include "tests_shims.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);
@@ -1245,10 +1601,13 @@
 new file mode 100644
 --- /dev/null
 +++ b/tests/core/proto/test_proto_tag_create.c
-@@ -0,0 +1,164 @@
+@@ -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 "tests_shims.h"
 +#include "tests_utils.h"
 +
 +#define MOCKED_ROUTER_TARGETS_TO_DEVICES
@@ -1256,32 +1615,12 @@
 +#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 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;
-+}
-+
-+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");
-+    }
-+
-+    return &devices;
-+}
++static struct lgtd_router_device_list device_1_only =
++    SLIST_HEAD_INITIALIZER(&device_1_only);
 +
 +static bool send_to_device_called = false;
 +
@@ -1312,17 +1651,121 @@
 +        errx(1, "missing SET_TAGS payload");
 +    }
 +
-+    struct lgtd_lifx_packet_tags *pkt_tags = pkt;
-+    if (pkt_tags->tags != 0x1) {
++    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)pkt_tags->tags, 0x2
++            (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)
 +{
@@ -1344,6 +1787,7 @@
 +    };
 +    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");
@@ -1375,15 +1819,13 @@
 +int
 +main(void)
 +{
-+    struct lgtd_client client;
-+    memset(&client, 0, sizeof(client));
++    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
 +
 +    setup_devices();
 +
-+    lgtd_proto_tag(&client, targets, "dub");
++    lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub");
 +
 +    const char expected[] = "true";
-+
 +    if (client_write_buf_idx != sizeof(expected) - 1) {
 +        lgtd_errx(
 +            1,
@@ -1393,7 +1835,6 @@
 +            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",
@@ -1401,19 +1842,518 @@
 +        );
 +    }
 +
++    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, "nothing was send to any device");
++        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 "tests_shims.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 "tests_shims.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
@@ -1422,6 +2362,7 @@
 +#include "proto.c"
 +
 +#include "mock_client_buf.h"
++#include "tests_shims.h"
 +#include "tests_utils.h"
 +
 +#define MOCKED_ROUTER_TARGETS_TO_DEVICES
@@ -1551,8 +2492,7 @@
 +int
 +main(void)
 +{
-+    struct lgtd_client client;
-+    memset(&client, 0, sizeof(client));
++    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
 +
 +    struct lgtd_proto_target_list *targets = (void *)0x2a;
 +
@@ -1596,6 +2536,7 @@
 +#include "proto.c"
 +
 +#include "mock_client_buf.h"
++#include "tests_shims.h"
 +#include "tests_utils.h"
 +
 +#define MOCKED_ROUTER_TARGETS_TO_DEVICES
@@ -1644,8 +2585,7 @@
 +int
 +main(void)
 +{
-+    struct lgtd_client client;
-+    memset(&client, 0, sizeof(client));
++    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
 +
 +    struct lgtd_proto_target_list *targets;
 +    targets = lgtd_tests_build_target_list("*", NULL);
@@ -1685,7 +2625,15 @@
 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
-@@ -32,6 +32,18 @@
+@@ -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
  
@@ -1729,7 +2677,7 @@
 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
-@@ -70,14 +70,65 @@
+@@ -70,14 +70,42 @@
      (void)pkt;
  }
  
@@ -1748,17 +2696,6 @@
 +    (void)pkt;
 +}
 +
-+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;
-+}
-+
 +void
 +lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id)
 +{
@@ -1781,18 +2718,6 @@
 +    return -1;
  }
 +
-+bool lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
-+                                    enum lgtd_lifx_packet_type pkt_type,
-+                                    const void *pkt,
-+                                    int pkt_size)
-+{
-+    (void)gw;
-+    (void)pkt_type;
-+    (void)pkt;
-+    (void)pkt_size;
-+    return false;
-+}
-+
 +void
 +lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw,
 +                                       uint64_t bulb_tags,
@@ -1802,6 +2727,38 @@
 +    (void)bulb_tags;
 +    (void)pkt_tags;
 +}
+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,27 @@
++#pragma once
++
++#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
 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
@@ -1815,16 +2772,27 @@
  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/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,103 @@
+@@ -0,0 +1,102 @@
++#include "gateway.c"
 +
 +#include <string.h>
 +
-+#include "gateway.c"
-+
 +#define MOCKED_LIFX_TAGGING_INCREF
 +#include "test_gateway_utils.h"
 +
@@ -2017,6 +2985,117 @@
 +
 +    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/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
--- a/tag_untag_testing_wip.patch	Sun Jul 19 17:39:41 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1176 +0,0 @@
-# HG changeset patch
-# Parent  ce5c778a6562f80ec8f2476632920dc738b67903
-
-diff --git a/core/proto.c b/core/proto.c
---- a/core/proto.c
-+++ b/core/proto.c
-@@ -263,7 +263,7 @@
-     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 builb
-+    // 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;
-@@ -279,10 +279,12 @@
-     // 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 = lgtd_lifx_gateway_get_tag_id(site->gw, tag);
-+        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, sizeof(pkt)
-         );
-@@ -300,9 +302,13 @@
-         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);
--        struct lgtd_lifx_packet_tags pkt;
--        pkt.tags = bulb->state.tags | LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
--        lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_TAGS, &pkt);
-+        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);
-@@ -360,6 +366,7 @@
-             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);
-             }
-         }
-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
-         );
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -405,6 +405,7 @@
-             struct lgtd_lifx_packet_tag_labels pkt = {
-                 .tags = ~(gw->tag_ids & ~tag_id)
-             };
-+            lgtd_lifx_wire_encode_tag_labels(&pkt);
-             lgtd_lifx_gateway_send_to_site(
-                 gw, LGTD_LIFX_SET_TAG_LABELS, &pkt, sizeof(pkt)
-             );
-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 "tests_shims.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 "tests_shims.h"
- 
- #define LGTD_TESTING_SET_LIGHT_FROM_HSBK
- #include "test_jsonrpc_utils.h"
-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 "tests_shims.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-@@ -34,8 +35,7 @@
-     };
-     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,
-@@ -80,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 "tests_shims.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 "tests_shims.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 "tests_shims.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_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 "tests_shims.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 "tests_shims.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 "tests_shims.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 "tests_shims.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 "tests_shims.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 "tests_shims.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 "tests_shims.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
---- a/tests/core/proto/test_proto_tag_create.c
-+++ b/tests/core/proto/test_proto_tag_create.c
-@@ -1,6 +1,9 @@
- #include "proto.c"
- 
- #include "mock_client_buf.h"
-+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
-+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID
-+#include "tests_shims.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-@@ -8,32 +11,12 @@
- #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 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;
--}
--
--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");
--    }
--
--    return &devices;
--}
-+static struct lgtd_router_device_list device_1_only =
-+    SLIST_HEAD_INITIALIZER(&device_1_only);
- 
- static bool send_to_device_called = false;
- 
-@@ -64,17 +47,129 @@
-         errx(1, "missing SET_TAGS payload");
-     }
- 
--    struct lgtd_lifx_packet_tags *pkt_tags = pkt;
--    if (pkt_tags->tags != 0x1) {
-+    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)pkt_tags->tags, 0x2
-+            (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,
-+                               int pkt_size)
-+{
-+    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);
-+    }
-+
-+    if (pkt_size != sizeof(*pkt_tag_labels)) {
-+        errx(
-+            1, "got pkt_size %d (expected %d)",
-+            pkt_size, (int)sizeof(*pkt_tag_labels)
-+        );
-+    }
-+
-+    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)
- {
-@@ -96,6 +191,7 @@
-     };
-     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");
-@@ -127,15 +223,13 @@
- int
- main(void)
- {
--    struct lgtd_client client;
--    memset(&client, 0, sizeof(client));
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
- 
-     setup_devices();
- 
--    lgtd_proto_tag(&client, targets, "dub");
-+    lgtd_proto_tag(&client, FAKE_TARGET_LIST, "dub");
- 
-     const char expected[] = "true";
--
-     if (client_write_buf_idx != sizeof(expected) - 1) {
-         lgtd_errx(
-             1,
-@@ -145,7 +239,6 @@
-             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",
-@@ -153,11 +246,14 @@
-         );
-     }
- 
-+    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, "nothing was send to any device");
-+        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
---- 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
-@@ -0,0 +1,210 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
-+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID
-+#include "tests_shims.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,
-+                               int pkt_size)
-+{
-+    (void)gw;
-+    (void)pkt_type;
-+    (void)pkt;
-+    (void)pkt_size;
-+
-+    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
---- a/tests/core/proto/test_proto_tag_update.c
-+++ b/tests/core/proto/test_proto_tag_update.c
-@@ -0,0 +1,290 @@
-+#include "proto.c"
-+
-+#include "mock_client_buf.h"
-+#define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
-+#define MOCKED_LIFX_GATEWAY_ALLOCATE_TAG_ID
-+#include "tests_shims.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,
-+                               int pkt_size)
-+{
-+    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 (pkt_size != sizeof(*pkt_tag_labels)) {
-+        errx(
-+            1, "got pkt_size %d (expected %d)",
-+            pkt_size, (int)sizeof(*pkt_tag_labels)
-+        );
-+    }
-+
-+    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
---- 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 "tests_shims.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-@@ -130,8 +131,7 @@
- int
- main(void)
- {
--    struct lgtd_client client;
--    memset(&client, 0, sizeof(client));
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
- 
-     struct lgtd_proto_target_list *targets = (void *)0x2a;
- 
-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 "tests_shims.h"
- #include "tests_utils.h"
- 
- #define MOCKED_ROUTER_TARGETS_TO_DEVICES
-@@ -49,8 +50,7 @@
- int
- main(void)
- {
--    struct lgtd_client client;
--    memset(&client, 0, sizeof(client));
-+    struct lgtd_client client = { .io = FAKE_BUFFEREVENT };
- 
-     struct lgtd_proto_target_list *targets;
-     targets = lgtd_tests_build_target_list("*", NULL);
-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)
- {
-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
-@@ -79,17 +79,6 @@
-     (void)pkt;
- }
- 
--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;
--}
--
- void
- lgtd_lifx_gateway_deallocate_tag_id(struct lgtd_lifx_gateway *gw, int tag_id)
- {
-@@ -111,18 +100,6 @@
-     return -1;
- }
- 
--bool lgtd_lifx_gateway_send_to_site(struct lgtd_lifx_gateway *gw,
--                                    enum lgtd_lifx_packet_type pkt_type,
--                                    const void *pkt,
--                                    int pkt_size)
--{
--    (void)gw;
--    (void)pkt_type;
--    (void)pkt;
--    (void)pkt_size;
--    return false;
--}
--
- void
- lgtd_lifx_gateway_update_tag_refcounts(struct lgtd_lifx_gateway *gw,
-                                        uint64_t bulb_tags,
-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,29 @@
-+#pragma once
-+
-+#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,
-+                               int pkt_size)
-+{
-+    (void)gw;
-+    (void)pkt_type;
-+    (void)pkt;
-+    (void)pkt_size;
-+    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
-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
-@@ -105,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;
- }