changeset 253:aecc19ba45d9

patch reorg
author Louis Opter <kalessin@kalessin.fr>
date Sun, 16 Aug 2015 00:48:06 -0700
parents 953786a62e00
children 320dbe44d3f6
files add_ipv4_to_ieee8023mac.patch add_start_stop_timer.patch blublu expose_a_bunch_of_metadata_in_get_light_state.patch fix_addrtoa_mess.patch fix_sockaddrtoa_mess.patch implement_some_metadata_packet_types.patch make_device_timeout_2_5x_force_refresh.patch series
diffstat 9 files changed, 1922 insertions(+), 2046 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/add_ipv4_to_ieee8023mac.patch	Sun Aug 16 00:48:06 2015 -0700
@@ -0,0 +1,118 @@
+# HG changeset patch
+# Parent  cb2ea88a7a426fb93e3c781fbc146126e574a9de
+
+diff --git a/core/proto.c b/core/proto.c
+--- a/core/proto.c
++++ b/core/proto.c
+@@ -202,11 +202,13 @@
+         struct lgtd_lifx_bulb *bulb = device->device;
+ 
+         char buf[2048],
++             gw_addr[LGTD_LIFX_ADDR_STRLEN],
+              site_addr[LGTD_LIFX_ADDR_STRLEN],
+              bulb_addr[LGTD_LIFX_ADDR_STRLEN];
+         int i = 0;
+ 
+         LGTD_IEEE8023MACTOA(bulb->addr, bulb_addr);
++        LGTD_IEEE8023MACTOA(bulb->gw->addr, gw_addr);
+         LGTD_IEEE8023MACTOA(bulb->gw->site.as_array, site_addr);
+ 
+         LGTD_SNPRINTF_APPEND(
+@@ -215,11 +217,12 @@
+                 "\"_lifx\":{"
+                     "\"addr\": \"%s\","
+                     "\"gateway\":{"
++                        "\"addr\":\"%s\","
+                         "\"site\":\"%s\","
+                         "\"url\":\"tcp://[%s]:%hu\","
+                         "\"latency\":%ju"
+                     "},",
+-            bulb_addr, site_addr,
++            bulb_addr, gw_addr, site_addr,
+             bulb->gw->ip_addr, bulb->gw->port,
+             (uintmax_t)LGTD_LIFX_GATEWAY_LATENCY(bulb->gw)
+         );
+diff --git a/lifx/gateway.c b/lifx/gateway.c
+--- a/lifx/gateway.c
++++ b/lifx/gateway.c
+@@ -15,12 +15,15 @@
+ // 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/ioctl.h>
+ #include <sys/queue.h>
++#include <sys/socket.h>
+ #include <sys/tree.h>
+ #include <assert.h>
+ #include <endian.h>
+ #include <err.h>
+ #include <errno.h>
++#include <net/if_arp.h>
+ #include <stdarg.h>
+ #include <stdbool.h>
+ #include <stdint.h>
+@@ -279,6 +282,28 @@
+     return bulb;
+ }
+ 
++// TODO: make a separate shared library to resolve ip addresses to mac
++// addresses:
++static bool
++lgtd_lifx_gateway_resolve_ipv4_addr(int socket,
++                                    const struct sockaddr_in *sin,
++                                    uint8_t *hw_addr)
++{
++    assert(sin);
++    assert(hw_addr);
++    assert(socket >= 0);
++
++    struct arpreq req;
++    memset(&req, 0, sizeof(req));
++    memcpy(&req.arp_pa, sin, sizeof(*sin));
++    memcpy(req.arp_dev, "br0", 4);
++    if (!ioctl(socket, SIOCGARP, &req)) {
++        memcpy(hw_addr, req.arp_ha.sa_data, LGTD_LIFX_ADDR_LENGTH);
++        return true;
++    }
++    return false;
++}
++
+ struct lgtd_lifx_gateway *
+ lgtd_lifx_gateway_open(const struct sockaddr_storage *peer,
+                        ev_socklen_t addrlen,
+@@ -314,10 +339,20 @@
+     gw->refresh_ev = evtimer_new(
+         lgtd_ev_base, lgtd_lifx_gateway_refresh_callback, gw
+     );
++
+     memcpy(&gw->peer, peer, sizeof(gw->peer));
+     lgtd_sockaddrtoa(peer, gw->ip_addr, sizeof(gw->ip_addr));
+     gw->port = lgtd_sockaddrport(peer);
+     memcpy(gw->site.as_array, site, sizeof(gw->site.as_array));
++    if (peer->ss_family == AF_INET) {
++        bool ok = lgtd_lifx_gateway_resolve_ipv4_addr(
++            gw->socket, (const struct sockaddr_in *)peer, gw->addr
++        );
++        if (!ok) {
++            lgtd_warn("couldn't resolve %s to a LIFX address", gw->ip_addr);
++        }
++    }
++
+     gw->last_req_at = received_at;
+     gw->next_req_at = received_at;
+     gw->last_pkt_at = received_at;
+@@ -332,11 +367,11 @@
+         goto error_allocate;
+     }
+ 
+-    char site_addr[LGTD_LIFX_ADDR_STRLEN];
++    char site_addr[LGTD_LIFX_ADDR_STRLEN], addr[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_info(
+-        "gateway for site %s at [%s]:%hu",
++        "gateway for site %s at [%s]:%hu (%s)",
+         LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr),
+-        gw->ip_addr, gw->port
++        gw->ip_addr, gw->port, LGTD_IEEE8023MACTOA(gw->addr, addr)
+     );
+     LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link);
+ 
--- a/add_start_stop_timer.patch	Sat Aug 15 23:22:19 2015 -0700
+++ b/add_start_stop_timer.patch	Sun Aug 16 00:48:06 2015 -0700
@@ -1,5 +1,5 @@
 # HG changeset patch
-# Parent  bddbeba717a2c01b12f3818c0d3b24ae72066f3c
+# Parent  e9bbea56e55f41d6bf2f774208408c8f7e9395bd
 Add an interface to start and stop standalone timers
 
 diff --git a/lifx/timer.c b/lifx/timer.c
@@ -100,7 +100,7 @@
 -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 = 3000 };
+-enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 5000 };
 -enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 2000 };
 +enum {
 +    LGTD_LIFX_TIMER_WATCHDOG_INTERVAL_MSECS = 500,
--- a/blublu	Sat Aug 15 23:22:19 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-# HG changeset patch
-# Parent  cf311c1e428a6bccd48b7fb7ca82715fb493ae0b
-
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -285,6 +285,7 @@
-                                     const struct sockaddr_in *sin,
-                                     uint8_t *hw_addr)
- {
-+#if 0
-     assert(sin);
-     assert(hw_addr);
-     assert(socket >= 0);
-@@ -296,6 +297,7 @@
-         memcpy(hw_addr, req.arp_ha.sa_data, LGTD_LIFX_ADDR_LENGTH);
-         return true;
-     }
-+#endif
-     return false;
- }
- 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/expose_a_bunch_of_metadata_in_get_light_state.patch	Sun Aug 16 00:48:06 2015 -0700
@@ -0,0 +1,1312 @@
+# HG changeset patch
+# Parent  20f6402b334b0f32e6f7b8efe65a421a3c5fdb97
+
+diff --git a/core/daemon.c b/core/daemon.c
+--- a/core/daemon.c
++++ b/core/daemon.c
+@@ -106,10 +106,9 @@
+     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 TITLE_APPEND(fmt, ...) LGTD_SNPRINTF_APPEND(    \
++    title, i, (int)sizeof(title), (fmt), __VA_ARGS__    \
++)
+ 
+ #define PREFIX(fmt, ...) TITLE_APPEND(                              \
+     "%s" fmt, (i && title[i - 1] == ')' ? "; " : ""), __VA_ARGS__   \
+diff --git a/core/lightsd.h b/core/lightsd.h
+--- a/core/lightsd.h
++++ b/core/lightsd.h
+@@ -24,10 +24,33 @@
+ #define LGTD_ABS(v) ((v) >= 0 ? (v) : (v) * -1)
+ #define LGTD_MIN(a, b) ((a) < (b) ? (a) : (b))
+ #define LGTD_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
+-#define LGTD_MSECS_TO_TIMEVAL(v) { \
++#define LGTD_MSECS_TO_TIMEVAL(v) {  \
+     .tv_sec = (v) / 1000,           \
+     .tv_usec = ((v) % 1000) * 1000  \
+ }
++#define LGTD_NSECS_TO_USECS(v) ((v) / (unsigned int)1E6)
++#define LGTD_NSECS_TO_SECS(v) ((v) / (unsigned int)1E9)
++#define LGTD_SECS_TO_NSECS(v) ((v) * (unsigned int)1E9)
++#define LGTD_TM_TO_ISOTIME(tm, sbuf, bufsz, usec) do {                          \
++    /* '2015-01-02T10:13:16.132222+00:00' */                                    \
++    if ((usec)) {                                                               \
++        snprintf(                                                               \
++            (sbuf), (bufsz), "%d-%02d-%02dT%02d:%02d:%02d.%jd%c%02ld:%02ld",    \
++            1900 + (tm)->tm_year, 1 + (tm)->tm_mon, (tm)->tm_mday,              \
++            (tm)->tm_hour, (tm)->tm_min, (tm)->tm_sec, (intmax_t)usec,          \
++            (tm)->tm_gmtoff >= 0 ? '+' : '-', /* %+02ld doesn't work */         \
++            LGTD_ABS((tm)->tm_gmtoff / 60 / 60), (tm)->tm_gmtoff % (60 * 60)    \
++        );                                                                      \
++    } else {                                                                    \
++        snprintf(                                                               \
++            (sbuf), (bufsz), "%d-%02d-%02dT%02d:%02d:%02d%c%02ld:%02ld",        \
++            1900 + (tm)->tm_year, 1 + (tm)->tm_mon, (tm)->tm_mday,              \
++            (tm)->tm_hour, (tm)->tm_min, (tm)->tm_sec,                          \
++            (tm)->tm_gmtoff >= 0 ? '+' : '-', /* %+02ld doesn't work */         \
++            LGTD_ABS((tm)->tm_gmtoff / 60 / 60), (tm)->tm_gmtoff % (60 * 60)    \
++        );                                                                      \
++    }                                                                           \
++} while (0)
+ #define LGTD_SNPRINTF_APPEND(buf, i, bufsz, ...) do {       \
+     int n = snprintf(&(buf)[(i)], bufsz - i, __VA_ARGS__);  \
+     (i) = LGTD_MIN((i) + n, bufsz);                         \
+@@ -57,6 +80,10 @@
+ void lgtd_sockaddrtoa(const struct sockaddr_storage *, char *buf, int buflen);
+ short lgtd_sockaddrport(const struct sockaddr_storage *);
+ 
++char *lgtd_print_duration(uint64_t, char *, int);
++#define LGTD_PRINT_DURATION(secs, arr) \
++    lgtd_print_duration((secs), (arr), sizeof((arr)))
++
+ void _lgtd_err(void (*)(int, const char *, ...), int, const char *, ...)
+     __attribute__((format(printf, 3, 4)));
+ #define lgtd_err(eval, fmt, ...) _lgtd_err(err, (eval), (fmt), ##__VA_ARGS__);
+diff --git a/core/log.c b/core/log.c
+--- a/core/log.c
++++ b/core/log.c
+@@ -53,14 +53,7 @@
+     if (!localtime_r(&now.tv_sec, &tm_now)) {
+         goto error;
+     }
+-    // '2015-01-02T10:13:16.132222+00:00'
+-    snprintf(
+-        strbuf, bufsz, "%d-%02d-%02dT%02d:%02d:%02d.%jd%c%02ld:%02ld",
+-        1900 + tm_now.tm_year, 1 + tm_now.tm_mon, tm_now.tm_mday,
+-        tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec,
+-        (intmax_t)now.tv_usec, tm_now.tm_gmtoff >= 0 ? '+' : '-', // %+02ld doesn't work
+-        LGTD_ABS(tm_now.tm_gmtoff / 60 / 60), tm_now.tm_gmtoff % (60 * 60)
+-    );
++    LGTD_TM_TO_ISOTIME(&tm_now, strbuf, bufsz, now.tv_usec);
+     return;
+ error:
+     strbuf[0] = '\0';
+@@ -121,6 +114,27 @@
+     }
+ }
+ 
++char *
++lgtd_print_duration(uint64_t secs, char *buf, int bufsz)
++{
++    assert(buf);
++    assert(bufsz > 0);
++
++    int days = secs / (60 * 60 * 24);
++    int minutes = secs / 60;
++    int hours = minutes / 60;
++    hours = hours % 24;
++    minutes = minutes % 60;
++
++    int i = 0;
++    if (days) {
++        int n = snprintf(buf, bufsz, "%d days ", days);
++        i = LGTD_MIN(i + n, bufsz);
++    }
++    snprintf(&buf[i], bufsz - i, "%02d:%02d", hours, minutes);
++    return buf;
++}
++
+ void
+ _lgtd_err(void (*errfn)(int, const char *, ...),
+            int eval,
+diff --git a/core/proto.c b/core/proto.c
+--- a/core/proto.c
++++ b/core/proto.c
+@@ -195,40 +195,116 @@
+         return;
+     }
+ 
+-    static const char *state_fmt = ("{"
+-        "\"hsbk\":[%s,%s,%s,%hu],"
+-        "\"power\":%s,"
+-        "\"label\":\"%s\","
+-        "\"tags\":[");
++    lgtd_client_start_send_response(client);
++    lgtd_client_write_string(client, "[");
++    struct lgtd_router_device *device;
++    SLIST_FOREACH(device, devices, link) {
++        struct lgtd_lifx_bulb *bulb = device->device;
++
++        char buf[2048],
++             site_addr[LGTD_LIFX_ADDR_STRLEN],
++             bulb_addr[LGTD_LIFX_ADDR_STRLEN];
++        int i = 0;
++
++        LGTD_IEEE8023MACTOA(bulb->addr, bulb_addr);
++        LGTD_IEEE8023MACTOA(bulb->gw->site.as_array, site_addr);
++
++        LGTD_SNPRINTF_APPEND(
++            buf, i, (int)sizeof(buf),
++            "{"
++                "\"_lifx\":{"
++                    "\"addr\": \"%s\","
++                    "\"gateway\":{"
++                        "\"site\":\"%s\","
++                        "\"url\":\"tcp://[%s]:%hu\","
++                        "\"latency\":%ju"
++                    "},",
++            bulb_addr, site_addr,
++            bulb->gw->ip_addr, bulb->gw->port,
++            (uintmax_t)LGTD_LIFX_GATEWAY_LATENCY(bulb->gw)
++        );
++
++#define PRINT_LIFX_FW_TIMESTAMPS(fw_info, built_at_buf, installed_at_buf)       \
++    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP((fw_info)->built_at, (built_at_buf));   \
++    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(                                        \
++        (fw_info)->installed_at, (installed_at_buf)                             \
++    )
++
++        for (int ip = 0; ip != LGTD_LIFX_BULB_IP_COUNT; ip++) {
++            char fw_built_at[64], fw_installed_at[64];
++            PRINT_LIFX_FW_TIMESTAMPS(
++                &bulb->ips[ip].fw_info, fw_built_at, fw_installed_at
++            );
++
++            LGTD_SNPRINTF_APPEND(
++                buf, i, (int)sizeof(buf),
++                "\"%s\":{"
++                    "\"firmware_built_at\":\"%s\","
++                    "\"firmware_installed_at\":\"%s\","
++                    "\"firmware_version\":\"%x\","
++                    "\"signal_strength\":%f,"
++                    "\"tx_bytes\":%u,"
++                    "\"rx_bytes\":%u,"
++                    "\"temperature\":%u"
++                "},",
++                lgtd_lifx_bulb_ip_names[ip],
++                fw_built_at, fw_installed_at, bulb->ips[ip].fw_info.version,
++                bulb->ips[ip].state.signal_strength,
++                bulb->ips[ip].state.tx_bytes,
++                bulb->ips[ip].state.rx_bytes,
++                bulb->ips[ip].state.temperature
++            );
++        }
++
++        LGTD_SNPRINTF_APPEND(
++            buf, i, (int)sizeof(buf),
++                "\"product_info\":{"
++                    "\"vendor_id\":\"%x\","
++                    "\"product_id\":\"%x\","
++                    "\"version\":%u"
++                "},",
++            bulb->product_info.vendor_id,
++            bulb->product_info.product_id,
++            bulb->product_info.version
++        );
++
++        char bulb_time[64];
++        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(bulb->runtime_info.time, bulb_time);
++        LGTD_SNPRINTF_APPEND(
++            buf, i, (int)sizeof(buf),
++                "\"runtime_info\":{"
++                    "\"time\":\"%s\","
++                    "\"uptime\":%ju,"
++                    "\"downtime\":%ju"
++                "}"
++            "},",
++            bulb_time,
++            (uintmax_t)LGTD_NSECS_TO_SECS(bulb->runtime_info.uptime),
++            (uintmax_t)LGTD_NSECS_TO_SECS(bulb->runtime_info.downtime)
++        );
+ 
+ #define PRINT_COMPONENT(src, dst, start, stop)          \
+     lgtd_jsonrpc_uint16_range_to_float_string(          \
+         (src), (start), (stop), (dst), sizeof((dst))    \
+     )
+ 
+-    lgtd_client_start_send_response(client);
+-    lgtd_client_write_string(client, "[");
+-    struct lgtd_router_device *device;
+-    SLIST_FOREACH(device, devices, link) {
+-        struct lgtd_lifx_bulb *bulb = device->device;
+-
+         char h[16], s[16], b[16];
+         PRINT_COMPONENT(bulb->state.hue, h, 0, 360);
+         PRINT_COMPONENT(bulb->state.saturation, s, 0, 1);
+         PRINT_COMPONENT(bulb->state.brightness, b, 0, 1);
+ 
+-        char buf[3072],
+-             bulb_addr[LGTD_LIFX_ADDR_STRLEN],
+-             site_addr[LGTD_LIFX_ADDR_STRLEN];
+-        LGTD_IEEE8023MACTOA(bulb->addr, bulb_addr);
+-        LGTD_IEEE8023MACTOA(bulb->gw->site.as_array, site_addr);
+-        int written = snprintf(
+-            buf, sizeof(buf), state_fmt,
++        LGTD_SNPRINTF_APPEND(
++            buf, i, (int)sizeof(buf),
++            "\"hsbk\":[%s,%s,%s,%hu],"
++            "\"power\":%s,"
++            "\"label\":\"%s\","
++            "\"tags\":[",
+             h, s, b, bulb->state.kelvin,
+             bulb->state.power == LGTD_LIFX_POWER_ON ? "true" : "false",
+             bulb->state.label[0] ? bulb->state.label : bulb_addr
+         );
+-        if (written >= (int)sizeof(buf)) {
++
++        if (i >= (int)sizeof(buf)) {
+             lgtd_warnx(
+                 "can't send state of bulb %s (%s) to client "
+                 "[%s]:%hu: output buffer to small",
+diff --git a/examples/lightsc.py b/examples/lightsc.py
+--- a/examples/lightsc.py
++++ b/examples/lightsc.py
+@@ -17,7 +17,7 @@
+         "id": str(uuid.uuid4()),
+     }
+     socket.send(json.dumps(payload).encode("utf-8"))
+-    response = socket.recv(2048).decode("utf-8")
++    response = socket.recv(8192).decode("utf-8")
+     try:
+         response = json.loads(response)
+     except ValueError:
+diff --git a/lifx/bulb.c b/lifx/bulb.c
+--- a/lifx/bulb.c
++++ b/lifx/bulb.c
+@@ -33,12 +33,57 @@
+ #include "bulb.h"
+ #include "gateway.h"
+ #include "core/daemon.h"
++#include "timer.h"
+ #include "core/stats.h"
++#include "core/jsmn.h"
++#include "core/jsonrpc.h"
++#include "core/client.h"
++#include "core/proto.h"
++#include "core/router.h"
+ #include "core/lightsd.h"
+ 
+ struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table =
+     RB_INITIALIZER(&lgtd_lifx_bulbs_table);
+ 
++const char * const lgtd_lifx_bulb_ip_names[] = { "mcu", "wifi" };
++
++static void
++lgtd_lifx_bulb_fetch_hardware_info(struct lgtd_lifx_timer *timer,
++                                   union lgtd_lifx_timer_ctx ctx)
++{
++    assert(timer);
++    assert(ctx.as_uint);
++
++    // Get the bulb again, it might have been closed while we were waiting:
++    const uint8_t *bulb_addr = (const uint8_t *)&ctx.as_uint;
++    struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_get(bulb_addr);
++    if (!bulb) {
++        lgtd_lifx_timer_stop_timer(timer);
++        return;
++    }
++
++#define RESEND_IF(test, pkt_type) do {                      \
++    if ((test)) {                                           \
++        stop = false;                                       \
++        lgtd_router_send_to_device(bulb, (pkt_type), NULL); \
++    }                                                       \
++} while (0)
++
++    bool stop = true;
++    RESEND_IF(!bulb->product_info.vendor_id, LGTD_LIFX_GET_VERSION);
++    RESEND_IF(
++        !bulb->ips[LGTD_LIFX_BULB_MCU_IP].fw_info.version,
++        LGTD_LIFX_GET_MESH_FIRMWARE
++    );
++    RESEND_IF(
++        !bulb->ips[LGTD_LIFX_BULB_WIFI_IP].fw_info.version,
++        LGTD_LIFX_GET_WIFI_FIRMWARE_STATE
++    );
++    if (stop) {
++        lgtd_lifx_timer_stop_timer(timer);
++    }
++}
++
+ struct lgtd_lifx_bulb *
+ lgtd_lifx_bulb_get(const uint8_t *addr)
+ {
+@@ -68,6 +113,14 @@
+ 
+     bulb->last_light_state_at = lgtd_time_monotonic_msecs();
+ 
++    union lgtd_lifx_timer_ctx ctx = { .as_uint = 0 };
++    memcpy(&ctx.as_uint, addr, LGTD_LIFX_ADDR_LENGTH);
++    lgtd_lifx_timer_start_timer(
++        LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS,
++        lgtd_lifx_bulb_fetch_hardware_info,
++        ctx
++    );
++
+     return bulb;
+ }
+ 
+@@ -124,6 +177,7 @@
+         LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(
+             bulbs_powered_on, state->power == LGTD_LIFX_POWER_ON ? 1 : -1
+         );
++        lgtd_router_send_to_device(bulb, LGTD_LIFX_GET_INFO, NULL);
+     }
+ 
+     lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, state->tags);
+@@ -141,6 +195,7 @@
+         LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(
+             bulbs_powered_on, power == LGTD_LIFX_POWER_ON ? 1 : -1
+         );
++        lgtd_router_send_to_device(bulb, LGTD_LIFX_GET_INFO, NULL);
+     }
+ 
+     bulb->state.power = power;
+@@ -155,3 +210,53 @@
+ 
+     bulb->state.tags = tags;
+ }
++
++void
++lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb,
++                      enum lgtd_lifx_bulb_ips ip_id,
++                      const struct lgtd_lifx_ip_state *state,
++                      lgtd_time_mono_t received_at)
++{
++    assert(bulb);
++    assert(state);
++
++    struct lgtd_lifx_bulb_ip *ip = &bulb->ips[ip_id];
++    ip->state_updated_at = received_at;
++    memcpy(&ip->state, state, sizeof(ip->state));
++}
++
++void
++lgtd_lifx_bulb_set_ip_firmware_info(struct lgtd_lifx_bulb *bulb,
++                                    enum lgtd_lifx_bulb_ips ip_id,
++                                    const struct lgtd_lifx_ip_firmware_info *info,
++                                    lgtd_time_mono_t received_at)
++{
++    assert(bulb);
++    assert(info);
++
++    struct lgtd_lifx_bulb_ip *ip = &bulb->ips[ip_id];
++    ip->fw_info_updated_at = received_at;
++    memcpy(&ip->fw_info, info, sizeof(ip->fw_info));
++}
++
++void
++lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *bulb,
++                                const struct lgtd_lifx_product_info *info)
++{
++    assert(bulb);
++    assert(info);
++
++    memcpy(&bulb->product_info, info, sizeof(bulb->product_info));
++}
++
++void
++lgtd_lifx_bulb_set_runtime_info(struct lgtd_lifx_bulb *bulb,
++                                const struct lgtd_lifx_runtime_info *info,
++                                lgtd_time_mono_t received_at)
++{
++    assert(bulb);
++    assert(info);
++
++    bulb->runtime_info_updated_at = received_at;
++    memcpy(&bulb->runtime_info, info, sizeof(bulb->runtime_info));
++}
+diff --git a/lifx/bulb.h b/lifx/bulb.h
+--- a/lifx/bulb.h
++++ b/lifx/bulb.h
+@@ -30,17 +30,64 @@
+     char        label[LGTD_LIFX_LABEL_SIZE];
+     uint64_t    tags;
+ };
++
++struct lgtd_lifx_ip_state {
++    float       signal_strength;    // mW
++    uint32_t    tx_bytes;
++    uint32_t    rx_bytes;
++    uint16_t    temperature;        // Deci-celcius: e.g 24.3 -> 2430
++};
++
++struct lgtd_lifx_ip_firmware_info {
++    uint64_t    built_at;       // ns since epoch
++    uint64_t    installed_at;   // ns since epoch
++    uint32_t    version;
++};
++
++struct lgtd_lifx_product_info {
++    uint32_t    vendor_id;
++    uint32_t    product_id;
++    uint32_t    version;
++};
++
++struct lgtd_lifx_runtime_info {
++    uint64_t    time;       // ns since epoch
++    uint64_t    uptime;     // ns
++    uint64_t    downtime;   // ns, last power off period duration
++};
+ #pragma pack(pop)
+ 
++enum { LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS = 5000 };
++
++enum lgtd_lifx_bulb_ips {
++    LGTD_LIFX_BULB_MCU_IP = 0,
++    LGTD_LIFX_BULB_WIFI_IP,
++    LGTD_LIFX_BULB_IP_COUNT,
++};
++
++// keyed with enum lgtd_lifx_bulb_ips:
++extern const char * const lgtd_lifx_bulb_ip_names[];
++
++struct lgtd_lifx_bulb_ip {
++    struct lgtd_lifx_ip_state           state;
++    lgtd_time_mono_t                    state_updated_at;
++    struct lgtd_lifx_ip_firmware_info   fw_info;
++    lgtd_time_mono_t                    fw_info_updated_at;
++};
++
+ struct lgtd_lifx_bulb {
+     RB_ENTRY(lgtd_lifx_bulb)        link;
+     SLIST_ENTRY(lgtd_lifx_bulb)     link_by_gw;
+-    struct lgtd_lifx_gateway        *gw;
+-    uint8_t                         addr[LGTD_LIFX_ADDR_LENGTH];
+-    struct lgtd_lifx_light_state    state;
+     lgtd_time_mono_t                last_light_state_at;
++    lgtd_time_mono_t                runtime_info_updated_at;
+     lgtd_time_mono_t                dirty_at;
+     uint16_t                        expected_power_on;
++    uint8_t                         addr[LGTD_LIFX_ADDR_LENGTH];
++    struct lgtd_lifx_gateway        *gw;
++    struct lgtd_lifx_light_state    state;
++    struct lgtd_lifx_bulb_ip        ips[LGTD_LIFX_BULB_IP_COUNT];
++    struct lgtd_lifx_product_info   product_info;
++    struct lgtd_lifx_runtime_info   runtime_info;
+ };
+ RB_HEAD(lgtd_lifx_bulb_map, lgtd_lifx_bulb);
+ SLIST_HEAD(lgtd_lifx_bulb_list, lgtd_lifx_bulb);
+@@ -69,3 +116,17 @@
+                                     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);
++
++void lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb,
++                                        enum lgtd_lifx_bulb_ips ip_id,
++                                        const struct lgtd_lifx_ip_state *state,
++                                        lgtd_time_mono_t received_at);
++void lgtd_lifx_bulb_set_ip_firmware_info(struct lgtd_lifx_bulb *bulb,
++                                         enum lgtd_lifx_bulb_ips ip_id,
++                                         const struct lgtd_lifx_ip_firmware_info *info,
++                                         lgtd_time_mono_t received_at);
++void lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *,
++                                     const struct lgtd_lifx_product_info *);
++void lgtd_lifx_bulb_set_runtime_info(struct lgtd_lifx_bulb *,
++                                     const struct lgtd_lifx_runtime_info *,
++                                     lgtd_time_mono_t);
+diff --git a/lifx/gateway.c b/lifx/gateway.c
+--- a/lifx/gateway.c
++++ b/lifx/gateway.c
+@@ -501,12 +501,8 @@
+         (uintmax_t)pkt->tags
+     );
+ 
+-    struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
+-        gw, hdr->target.device_addr
+-    );
+-    if (!b) {
+-        return;
+-    }
++    struct lgtd_lifx_bulb *b;
++    LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, hdr->target.device_addr);
+ 
+     assert(sizeof(*pkt) == sizeof(b->state));
+     lgtd_lifx_bulb_set_light_state(
+@@ -531,7 +527,7 @@
+         }
+     }
+ 
+-    int latency = gw->last_pkt_at - gw->last_req_at;
++    int latency = LGTD_LIFX_GATEWAY_LATENCY(gw);
+     if (latency < LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS) {
+         if (!event_pending(gw->refresh_ev, EV_TIMEOUT, NULL)) {
+             int timeout = LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS - latency;
+@@ -573,14 +569,9 @@
+         LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), pkt->power
+     );
+ 
+-    struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
+-        gw, hdr->target.device_addr
++    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
++        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_power_state, pkt->power
+     );
+-    if (!b) {
+-        return;
+-    }
+-
+-    lgtd_lifx_bulb_set_power_state(b, pkt->power);
+ }
+ 
+ int
+@@ -691,9 +682,10 @@
+     }
+ }
+ 
+-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
++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);
+ 
+@@ -705,12 +697,8 @@
+         (uintmax_t)pkt->tags
+     );
+ 
+-    struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
+-        gw, hdr->target.device_addr
+-    );
+-    if (!b) {
+-        return;
+-    }
++    struct lgtd_lifx_bulb *b;
++    LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, hdr->target.device_addr);
+ 
+     char bulb_addr[LGTD_LIFX_ADDR_STRLEN], site_addr[LGTD_LIFX_ADDR_STRLEN];
+     int tag_id;
+@@ -729,3 +717,132 @@
+ 
+     lgtd_lifx_bulb_set_tags(b, pkt->tags);
+ }
++
++void
++lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *gw,
++                                  const struct lgtd_lifx_packet_header *hdr,
++                                  const struct lgtd_lifx_packet_ip_state *pkt)
++{
++    assert(gw && hdr && pkt);
++
++    const char  *type;
++    enum lgtd_lifx_bulb_ips ip_id;
++    switch (hdr->packet_type) {
++    case LGTD_LIFX_MESH_INFO:
++        type = "MCU_STATE";
++        ip_id = LGTD_LIFX_BULB_MCU_IP;
++        break;
++    case LGTD_LIFX_WIFI_INFO:
++        type = "WIFI_STATE";
++        ip_id = LGTD_LIFX_BULB_WIFI_IP;
++        break;
++    default:
++        lgtd_info("invalid ip state packet_type %#hx", hdr->packet_type);
++#ifndef NDEBUG
++        abort();
++#endif
++        return;
++    }
++
++    char addr[LGTD_LIFX_ADDR_STRLEN];
++    lgtd_debug(
++        "%s <-- [%s]:%hu - %s "
++        "signal_strength=%f, rx_bytes=%u, tx_bytes=%u, temperature=%hu",
++        type, gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
++        pkt->signal_strength, pkt->rx_bytes, pkt->tx_bytes, pkt->temperature
++    );
++
++    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
++        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_ip_state,
++        ip_id, (const struct lgtd_lifx_ip_state *)pkt, gw->last_pkt_at
++    );
++}
++
++void
++lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *gw,
++                                          const struct lgtd_lifx_packet_header *hdr,
++                                          const struct lgtd_lifx_packet_ip_firmware_info *pkt)
++{
++    assert(gw && hdr && pkt);
++
++    const char  *type;
++    enum lgtd_lifx_bulb_ips ip_id;
++    switch (hdr->packet_type) {
++    case LGTD_LIFX_MESH_FIRMWARE:
++        type = "MCU_FIRMWARE_INFO";
++        ip_id = LGTD_LIFX_BULB_MCU_IP;
++        break;
++    case LGTD_LIFX_WIFI_FIRMWARE_STATE:
++        type = "WIFI_FIRMWARE_INFO";
++        ip_id = LGTD_LIFX_BULB_WIFI_IP;
++        break;
++    default:
++        lgtd_info("invalid ip firmware packet_type %#hx", hdr->packet_type);
++#ifndef NDEBUG
++        abort();
++#endif
++        return;
++    }
++
++    char built_at[64], installed_at[64], addr[LGTD_LIFX_ADDR_STRLEN];
++    lgtd_debug(
++        "%s <-- [%s]:%hu - %s "
++        "built_at=%s, installed_at=%s, version=%u",
++        type, gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
++        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->built_at, built_at),
++        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->installed_at, installed_at),
++        pkt->version
++    );
++
++    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
++        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_ip_firmware_info,
++        ip_id, (const struct lgtd_lifx_ip_firmware_info *)pkt, gw->last_pkt_at
++    );
++}
++
++void
++lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *gw,
++                                      const struct lgtd_lifx_packet_header *hdr,
++                                      const struct lgtd_lifx_packet_product_info *pkt)
++{
++    assert(gw && hdr && pkt);
++
++    char addr[LGTD_LIFX_ADDR_STRLEN];
++    lgtd_debug(
++        "PRODUCT_INFO <-- [%s]:%hu - %s "
++        "vendor_id=%#x, product_id=%#x, version=%u",
++        gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
++        pkt->vendor_id, pkt->product_id, pkt->version
++    );
++
++    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
++        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_product_info,
++        (const struct lgtd_lifx_product_info *)pkt
++    );
++}
++
++void
++lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *gw,
++                                      const struct lgtd_lifx_packet_header *hdr,
++                                      const struct lgtd_lifx_packet_runtime_info *pkt)
++{
++    assert(gw && hdr && pkt);
++
++    char device_time[64], uptime[64], downtime[64], addr[LGTD_LIFX_ADDR_STRLEN];
++    lgtd_debug(
++        "PRODUCT_INFO <-- [%s]:%hu - %s time=%s, uptime=%s, downtime=%s",
++        gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
++        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->time, device_time),
++        LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->uptime), uptime),
++        LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->downtime), downtime)
++    );
++
++    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
++        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_runtime_info,
++        (const struct lgtd_lifx_runtime_info *)pkt, gw->last_pkt_at
++    );
++}
+diff --git a/lifx/gateway.h b/lifx/gateway.h
+--- a/lifx/gateway.h
++++ b/lifx/gateway.h
+@@ -36,12 +36,19 @@
+ struct lgtd_lifx_gateway {
+     LIST_ENTRY(lgtd_lifx_gateway)   link;
+     struct lgtd_lifx_bulb_list      bulbs;
++#define LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, bulb_addr)  do {    \
++    (b) = lgtd_lifx_gateway_get_or_open_bulb((gw), (bulb_addr));        \
++    if (!(b)) {                                                         \
++        return;                                                         \
++    }                                                                   \
++} while (0)
+     // Multiple gateways can share the same site (that happens when bulbs are
+     // far away enough that ZigBee can't be used). Moreover the SET_PAN_GATEWAY
+     // packet doesn't include the device address in the header (i.e: site and
+     // device_addr have the same value) so we have no choice but to use the
+     // remote ip address to identify a gateway:
+     struct sockaddr_storage         peer;
++    uint8_t                         addr[LGTD_LIFX_ADDR_LENGTH];
+     char                            ip_addr[INET6_ADDRSTRLEN];
+     uint16_t                        port;
+     // TODO: just use an integer and rename it to site_id:
+@@ -60,6 +67,7 @@
+     lgtd_time_mono_t                last_req_at;
+     lgtd_time_mono_t                next_req_at;
+     lgtd_time_mono_t                last_pkt_at;
++#define LGTD_LIFX_GATEWAY_LATENCY(gw) ((gw)->last_pkt_at - (gw)->last_req_at)
+     struct lgtd_lifx_message        pkt_ring[LGTD_LIFX_GATEWAY_PACKET_RING_SIZE];
+ #define LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(idx)  do { \
+     (idx) += 1;                                             \
+@@ -77,6 +85,12 @@
+ 
+ extern struct lgtd_lifx_gateway_list lgtd_lifx_gateways;
+ 
++#define LGTD_LIFX_GATEWAY_SET_BULB_ATTR(gw, bulb_addr, bulb_fn, ...) do {   \
++    struct lgtd_lifx_bulb *b;                                               \
++    LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, bulb_addr);                 \
++    (bulb_fn)(b, __VA_ARGS__);                                              \
++} while (0)
++
+ struct lgtd_lifx_gateway *lgtd_lifx_gateway_get(const struct sockaddr_storage *);
+ struct lgtd_lifx_gateway *lgtd_lifx_gateway_open(const struct sockaddr_storage *,
+                                                  ev_socklen_t,
+@@ -119,3 +133,15 @@
+ void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *,
+                                    const struct lgtd_lifx_packet_header *,
+                                    const struct lgtd_lifx_packet_tags *);
++void lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *,
++                                        const struct lgtd_lifx_packet_header *,
++                                        const struct lgtd_lifx_packet_ip_state *);
++void lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *,
++                                               const struct lgtd_lifx_packet_header *,
++                                               const struct lgtd_lifx_packet_ip_firmware_info *);
++void lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *,
++                                           const struct lgtd_lifx_packet_header *,
++                                           const struct lgtd_lifx_packet_product_info *);
++void lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *,
++                                           const struct lgtd_lifx_packet_header *,
++                                           const struct lgtd_lifx_packet_runtime_info *);
+diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
+--- a/lifx/wire_proto.c
++++ b/lifx/wire_proto.c
+@@ -24,8 +24,10 @@
+ #include <stdarg.h>
+ #include <stdbool.h>
+ #include <stdint.h>
++#include <stdio.h>
+ #include <stdlib.h>
+ #include <string.h>
++#include <time.h>
+ 
+ #include <event2/util.h>
+ 
+@@ -74,12 +76,32 @@
+     (void)pkt;
+ }
+ 
++static void
++lgtd_lifx_wire_enosys_packet_handler(struct lgtd_lifx_gateway *gw,
++                                     const struct lgtd_lifx_packet_header *hdr,
++                                     const void *pkt)
++{
++    (void)pkt;
++
++    const struct lgtd_lifx_packet_info *pkt_info;
++    pkt_info = lgtd_lifx_wire_get_packet_info(hdr->packet_type);
++    bool addressable = hdr->protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE;
++    bool tagged = hdr->protocol & LGTD_LIFX_PROTOCOL_TAGGED;
++    unsigned int protocol = hdr->protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK;
++    lgtd_info(
++        "%s <-- [%s]:%hu - (Unimplemented, header info: "
++        "addressable=%d, tagged=%d, protocol=%d)",
++        pkt_info->name, gw->ip_addr, gw->port,
++        addressable, tagged, protocol
++    );
++}
++
+ void
+ lgtd_lifx_wire_load_packet_info_map(void)
+ {
+ #define DECODER(x)  ((void (*)(void *))(x))
+ #define ENCODER(x)  ((void (*)(void *))(x))
+-#define HANDLER(x)                                  \
++#define HANDLER(x)                                      \
+     ((void (*)(struct lgtd_lifx_gateway *,              \
+                const struct lgtd_lifx_packet_header *,  \
+                const void *))(x))
+@@ -90,6 +112,10 @@
+ #define REQUEST_ONLY                                        \
+     .decode = lgtd_lifx_wire_null_packet_encoder_decoder,   \
+     .handle = lgtd_lifx_wire_null_packet_handler
++#define UNIMPLEMENTED                                       \
++    .decode = lgtd_lifx_wire_null_packet_encoder_decoder,   \
++    .encode = lgtd_lifx_wire_null_packet_encoder_decoder,   \
++    .handle = lgtd_lifx_wire_enosys_packet_handler
+ 
+     static struct lgtd_lifx_packet_info packet_table[] = {
+         // Gateway packets:
+@@ -187,6 +213,211 @@
+             .size = sizeof(struct lgtd_lifx_packet_tags),
+             .decode = DECODER(lgtd_lifx_wire_decode_tags),
+             .handle = HANDLER(lgtd_lifx_gateway_handle_tags)
++        },
++        {
++            REQUEST_ONLY,
++            NO_PAYLOAD,
++            .name = "GET_MESH_INFO",
++            .type = LGTD_LIFX_GET_MESH_INFO
++        },
++        {
++            RESPONSE_ONLY,
++            .name = "MESH_INFO",
++            .type = LGTD_LIFX_MESH_INFO,
++            .size = sizeof(struct lgtd_lifx_packet_ip_state),
++            .decode = DECODER(lgtd_lifx_wire_decode_ip_state),
++            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_state)
++        },
++        {
++            REQUEST_ONLY,
++            NO_PAYLOAD,
++            .name = "GET_MESH_FIRMWARE",
++            .type = LGTD_LIFX_GET_MESH_FIRMWARE
++        },
++        {
++            RESPONSE_ONLY,
++            .name = "MESH_FIRMWARE",
++            .type = LGTD_LIFX_MESH_FIRMWARE,
++            .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info),
++            .decode = DECODER(lgtd_lifx_wire_decode_ip_firmware_info),
++            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_firmware_info)
++        },
++        {
++            REQUEST_ONLY,
++            NO_PAYLOAD,
++            .name = "GET_WIFI_INFO",
++            .type = LGTD_LIFX_GET_WIFI_INFO,
++        },
++        {
++            RESPONSE_ONLY,
++            .name = "WIFI_INFO",
++            .type = LGTD_LIFX_WIFI_INFO,
++            .size = sizeof(struct lgtd_lifx_packet_ip_state),
++            .decode = DECODER(lgtd_lifx_wire_decode_ip_state),
++            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_state)
++        },
++        {
++            REQUEST_ONLY,
++            NO_PAYLOAD,
++            .name = "GET_WIFI_FIRMWARE_STATE",
++            .type = LGTD_LIFX_GET_WIFI_FIRMWARE_STATE
++        },
++        {
++            RESPONSE_ONLY,
++            .name = "WIFI_FIRMWARE_STATE",
++            .type = LGTD_LIFX_WIFI_FIRMWARE_STATE,
++            .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info),
++            .decode = DECODER(lgtd_lifx_wire_decode_ip_firmware_info),
++            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_firmware_info)
++        },
++        {
++            REQUEST_ONLY,
++            NO_PAYLOAD,
++            .name = "GET_VERSION",
++            .type = LGTD_LIFX_GET_VERSION
++        },
++        {
++            RESPONSE_ONLY,
++            .name = "VERSION_STATE",
++            .type = LGTD_LIFX_VERSION_STATE,
++            .size = sizeof(struct lgtd_lifx_packet_product_info),
++            .decode = DECODER(lgtd_lifx_wire_decode_product_info),
++            .handle = HANDLER(lgtd_lifx_gateway_handle_product_info)
++        },
++        {
++            REQUEST_ONLY,
++            NO_PAYLOAD,
++            .name = "GET_INFO",
++            .type = LGTD_LIFX_GET_INFO
++        },
++        {
++            RESPONSE_ONLY,
++            .name = "INFO_STATE",
++            .type = LGTD_LIFX_INFO_STATE,
++            .size = sizeof(struct lgtd_lifx_packet_runtime_info),
++            .decode = DECODER(lgtd_lifx_wire_decode_runtime_info),
++            .handle = HANDLER(lgtd_lifx_gateway_handle_runtime_info)
++        },
++        // Unimplemented but "known" packets
++        {
++            UNIMPLEMENTED,
++            .name = "GET_TIME",
++            .type = LGTD_LIFX_GET_TIME
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "SET_TIME",
++            .type = LGTD_LIFX_SET_TIME
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "TIME_STATE",
++            .type = LGTD_LIFX_TIME_STATE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "GET_RESET_SWITCH_STATE",
++            .type = LGTD_LIFX_GET_RESET_SWITCH_STATE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "RESET_SWITCH_STATE",
++            .type = LGTD_LIFX_RESET_SWITCH_STATE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "GET_BULB_LABEL",
++            .type = LGTD_LIFX_GET_BULB_LABEL
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "SET_BULB_LABEL",
++            .type = LGTD_LIFX_SET_BULB_LABEL
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "BULB_LABEL",
++            .type = LGTD_LIFX_BULB_LABEL
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "GET_MCU_RAIL_VOLTAGE",
++            .type = LGTD_LIFX_GET_MCU_RAIL_VOLTAGE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "MCU_RAIL_VOLTAGE",
++            .type = LGTD_LIFX_MCU_RAIL_VOLTAGE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "REBOOT",
++            .type = LGTD_LIFX_REBOOT
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "SET_FACTORY_TEST_MODE",
++            .type = LGTD_LIFX_SET_FACTORY_TEST_MODE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "DISABLE_FACTORY_TEST_MODE",
++            .type = LGTD_LIFX_DISABLE_FACTORY_TEST_MODE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "ACK",
++            .type = LGTD_LIFX_ACK
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "ECHO_REQUEST",
++            .type = LGTD_LIFX_ECHO_REQUEST
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "ECHO_RESPONSE",
++            .type = LGTD_LIFX_ECHO_RESPONSE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "SET_DIM_ABSOLUTE",
++            .type = LGTD_LIFX_SET_DIM_ABSOLUTE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "SET_DIM_RELATIVE",
++            .type = LGTD_LIFX_SET_DIM_RELATIVE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "GET_WIFI_STATE",
++            .type = LGTD_LIFX_GET_WIFI_STATE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "SET_WIFI_STATE",
++            .type = LGTD_LIFX_SET_WIFI_STATE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "WIFI_STATE",
++            .type = LGTD_LIFX_WIFI_STATE
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "GET_ACCESS_POINTS",
++            .type = LGTD_LIFX_GET_ACCESS_POINTS
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "SET_ACCESS_POINTS",
++            .type = LGTD_LIFX_SET_ACCESS_POINTS
++        },
++        {
++            UNIMPLEMENTED,
++            .name = "ACCESS_POINT",
++            .type = LGTD_LIFX_ACCESS_POINT
+         }
+     };
+ 
+@@ -234,6 +465,25 @@
+     return LGTD_LIFX_WAVEFORM_INVALID;
+ }
+ 
++char *
++lgtd_lifx_wire_print_nsec_timestamp(uint64_t nsec_ts, char *buf, int bufsz)
++{
++    assert(buf);
++    assert(bufsz > 0);
++
++    time_t ts = LGTD_NSECS_TO_SECS(nsec_ts);
++
++    struct tm tm_utc;
++    if (gmtime_r(&ts, &tm_utc)) {
++        int64_t usecs = LGTD_NSECS_TO_USECS(nsec_ts - LGTD_SECS_TO_NSECS(ts));
++        LGTD_TM_TO_ISOTIME(&tm_utc, buf, bufsz, usecs);
++    } else {
++        buf[0] = '\0';
++    }
++
++    return buf;
++}
++
+ static void
+ lgtd_lifx_wire_encode_header(struct lgtd_lifx_packet_header *hdr, int flags)
+ {
+@@ -423,3 +673,44 @@
+ 
+     pkt->tags = le64toh(pkt->tags);
+ }
++
++void
++lgtd_lifx_wire_decode_ip_state(struct lgtd_lifx_packet_ip_state *pkt)
++{
++    assert(pkt);
++
++    pkt->signal_strength = lgtd_lifx_wire_lefloattoh(pkt->signal_strength);
++    pkt->tx_bytes = le32toh(pkt->tx_bytes);
++    pkt->rx_bytes = le32toh(pkt->rx_bytes);
++    pkt->temperature = le16toh(pkt->temperature);
++}
++
++void
++lgtd_lifx_wire_decode_ip_firmware_info(struct lgtd_lifx_packet_ip_firmware_info *pkt)
++{
++    assert(pkt);
++
++    pkt->built_at = le64toh(pkt->built_at);
++    pkt->installed_at = le64toh(pkt->installed_at);
++    pkt->version = le32toh(pkt->version);
++}
++
++void
++lgtd_lifx_wire_decode_product_info(struct lgtd_lifx_packet_product_info *pkt)
++{
++    assert(pkt);
++
++    pkt->vendor_id = le32toh(pkt->vendor_id);
++    pkt->product_id = le32toh(pkt->product_id);
++    pkt->version = le32toh(pkt->version);
++}
++
++void
++lgtd_lifx_wire_decode_runtime_info(struct lgtd_lifx_packet_runtime_info *pkt)
++{
++    assert(pkt);
++
++    pkt->time = le64toh(pkt->time);
++    pkt->uptime = le64toh(pkt->uptime);
++    pkt->downtime = le64toh(pkt->downtime);
++}
+diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
+--- a/lifx/wire_proto.h
++++ b/lifx/wire_proto.h
+@@ -28,7 +28,7 @@
+ static inline floatle_t
+ lgtd_lifx_wire_htolefloat(float f)
+ {
+-    union { float f; uint32_t i; } u  = { .f = f };
++    union { float f; uint32_t i; } u = { .f = f };
+     htole32(u.i);
+     return u.f;
+ }
+@@ -36,7 +36,7 @@
+ static inline floatle_t
+ lgtd_lifx_wire_lefloattoh(float f)
+ {
+-    union { float f; uint32_t i; } u  = { .f = f };
++    union { float f; uint32_t i; } u = { .f = f };
+     le32toh(u.i);
+     return u.f;
+ }
+@@ -250,6 +250,30 @@
+     char        label[LGTD_LIFX_LABEL_SIZE];
+ };
+ 
++struct lgtd_lifx_packet_ip_state {
++    floatle_t   signal_strength;
++    uint32le_t  tx_bytes;
++    uint32le_t  rx_bytes;
++    uint16le_t  temperature;
++};
++
++struct lgtd_lifx_packet_ip_firmware_info {
++    uint64le_t  built_at;
++    uint64le_t  installed_at;
++    uint32le_t  version;
++};
++
++struct lgtd_lifx_packet_product_info {
++    uint32le_t  vendor_id;
++    uint32le_t  product_id;
++    uint32le_t  version;
++};
++
++struct lgtd_lifx_packet_runtime_info {
++    uint64le_t  time;
++    uint64le_t  uptime;
++    uint64le_t  downtime;
++};
+ #pragma pack(pop)
+ 
+ enum lgtd_lifx_header_flags {
+@@ -331,8 +355,10 @@
+          (tag_id_varname) != -1;                                                    \
+          (tag_id_varname) = lgtd_lifx_wire_next_tag_id((tag_id_varname), (tags)))
+ 
+-
+ enum lgtd_lifx_waveform_type lgtd_lifx_wire_waveform_string_id_to_type(const char *, int);
++char* lgtd_lifx_wire_print_nsec_timestamp(uint64_t, char *, int);
++#define LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(ts, arr) \
++    lgtd_lifx_wire_print_nsec_timestamp((ts), (arr), sizeof((arr)))
+ 
+ const struct lgtd_lifx_packet_info *lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type);
+ void lgtd_lifx_wire_load_packet_info_map(void);
+@@ -357,3 +383,8 @@
+ 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 *);
++
++void lgtd_lifx_wire_decode_ip_state(struct lgtd_lifx_packet_ip_state *);
++void lgtd_lifx_wire_decode_ip_firmware_info(struct lgtd_lifx_packet_ip_firmware_info *);
++void lgtd_lifx_wire_decode_product_info(struct lgtd_lifx_packet_product_info *);
++void lgtd_lifx_wire_decode_runtime_info(struct lgtd_lifx_packet_runtime_info *);
+diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h
+--- a/tests/lifx/mock_gateway.h
++++ b/tests/lifx/mock_gateway.h
+@@ -129,3 +129,51 @@
+     (void)pkt_tags;
+ }
+ #endif
++
++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_IP_STATE
++void
++lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *gw,
++                                  const struct lgtd_lifx_packet_header *hdr,
++                                  const struct lgtd_lifx_packet_ip_state *pkt)
++{
++    (void)gw;
++    (void)hdr;
++    (void)pkt;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_IP_FIRMWARE_INFO
++void
++lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *gw,
++                                          const struct lgtd_lifx_packet_header *hdr,
++                                          const struct lgtd_lifx_packet_ip_firmware_info *pkt)
++{
++    (void)gw;
++    (void)hdr;
++    (void)pkt;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PRODUCT_INFO
++void
++lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *gw,
++                                      const struct lgtd_lifx_packet_header *hdr,
++                                      const struct lgtd_lifx_packet_product_info *pkt)
++{
++    (void)gw;
++    (void)hdr;
++    (void)pkt;
++}
++#endif
++
++#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_RUNTIME_INFO
++void
++lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *gw,
++                                      const struct lgtd_lifx_packet_header *hdr,
++                                      const struct lgtd_lifx_packet_runtime_info *pkt)
++{
++    (void)gw;
++    (void)hdr;
++    (void)pkt;
++}
++#endif
+diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
+--- a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
++++ b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
+@@ -2,7 +2,7 @@
+ 
+ #include "wire_proto.c"
+ 
+-#include "test_wire_proto_utils.h"
++#include "mock_gateway.h"
+ 
+ int
+ main(void)
+diff --git a/tests/lifx/wire_proto/test_wire_proto_utils.h b/tests/lifx/wire_proto/test_wire_proto_utils.h
+deleted file mode 100644
+--- a/tests/lifx/wire_proto/test_wire_proto_utils.h
++++ /dev/null
+@@ -1,46 +0,0 @@
+-#pragma once
+-
+-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;
+-}
+-
+-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/lifx/wire_proto/test_wire_proto_waveform_table.c b/tests/lifx/wire_proto/test_wire_proto_waveform_table.c
+--- a/tests/lifx/wire_proto/test_wire_proto_waveform_table.c
++++ b/tests/lifx/wire_proto/test_wire_proto_waveform_table.c
+@@ -1,6 +1,6 @@
+ #include "wire_proto.c"
+ 
+-#include "test_wire_proto_utils.h"
++#include "mock_gateway.h"
+ 
+ int
+ main(void)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fix_addrtoa_mess.patch	Sun Aug 16 00:48:06 2015 -0700
@@ -0,0 +1,468 @@
+# HG changeset patch
+# Parent  13b24c5af219c96e98584529bf917d7cef143ab2
+
+diff --git a/core/lightsd.h b/core/lightsd.h
+--- a/core/lightsd.h
++++ b/core/lightsd.h
+@@ -51,7 +51,9 @@
+ extern struct lgtd_opts lgtd_opts;
+ extern struct event_base *lgtd_ev_base;
+ 
+-const char *lgtd_addrtoa(const uint8_t *);
++char *lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen);
++#define LGTD_IEEE8023MACTOA(addr, buf) \
++    lgtd_iee8023mactoa((addr), (buf), sizeof(buf))
+ void lgtd_sockaddrtoa(const struct sockaddr_storage *, char *buf, int buflen);
+ short lgtd_sockaddrport(const struct sockaddr_storage *);
+ 
+diff --git a/core/log.c b/core/log.c
+--- a/core/log.c
++++ b/core/log.c
+@@ -81,17 +81,18 @@
+     fprintf(stderr, "[%s] %s", loglvl, showprogname ? "lightsd: " : "");
+ }
+ 
+-const char *
+-lgtd_addrtoa(const uint8_t *addr)
++char *
++lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen)
+ {
+     assert(addr);
++    assert(buf);
++    assert(buflen >= 2 * 6 + 5 + 1);
+ 
+-    static char str[LGTD_LIFX_ADDR_LENGTH * 2 + LGTD_LIFX_ADDR_LENGTH - 1 + 1];
+     snprintf(
+-        str, sizeof(str), "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
++        buf, buflen, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
+         addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]
+     );
+-    return str;
++    return buf;
+ }
+ 
+ void
+diff --git a/core/proto.c b/core/proto.c
+--- a/core/proto.c
++++ b/core/proto.c
+@@ -217,19 +217,22 @@
+         PRINT_COMPONENT(bulb->state.saturation, s, 0, 1);
+         PRINT_COMPONENT(bulb->state.brightness, b, 0, 1);
+ 
+-        char buf[3072];
++        char buf[3072],
++             bulb_addr[LGTD_LIFX_ADDR_STRLEN],
++             site_addr[LGTD_LIFX_ADDR_STRLEN];
++        LGTD_IEEE8023MACTOA(bulb->addr, bulb_addr);
++        LGTD_IEEE8023MACTOA(bulb->gw->site.as_array, site_addr);
+         int written = snprintf(
+             buf, sizeof(buf), state_fmt,
+             h, s, b, bulb->state.kelvin,
+             bulb->state.power == LGTD_LIFX_POWER_ON ? "true" : "false",
+-            bulb->state.label[0] ? bulb->state.label : lgtd_addrtoa(bulb->addr)
++            bulb->state.label[0] ? bulb->state.label : bulb_addr
+         );
+         if (written >= (int)sizeof(buf)) {
+             lgtd_warnx(
+                 "can't send state of bulb %s (%s) to client "
+                 "[%s]:%hu: output buffer to small",
+-                bulb->state.label, lgtd_addrtoa(bulb->addr),
+-                client->ip_addr, client->port
++                bulb->state.label, bulb_addr, client->ip_addr, client->port
+             );
+             continue;
+         }
+@@ -248,8 +251,7 @@
+                     "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)
++                    bulb_addr, bulb->gw->ip_addr, bulb->gw->port, site_addr
+                 );
+             }
+         }
+diff --git a/core/router.c b/core/router.c
+--- a/core/router.c
++++ b/core/router.c
+@@ -106,7 +106,9 @@
+         bulb->expected_power_on = payload->power;
+     }
+ 
+-    lgtd_info("sending %s to %s", pkt_info->name, lgtd_addrtoa(bulb->addr));
++    char addr[LGTD_LIFX_ADDR_STRLEN];
++    LGTD_IEEE8023MACTOA(bulb->addr, addr);
++    lgtd_info("sending %s to %s", pkt_info->name, addr);
+ }
+ 
+ void
+diff --git a/lifx/bulb.c b/lifx/bulb.c
+--- a/lifx/bulb.c
++++ b/lifx/bulb.c
+@@ -100,11 +100,12 @@
+         LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1);
+     }
+     RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb);
++    char addr[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_info(
+         "closed bulb \"%.*s\" (%s) on [%s]:%hu",
+         LGTD_LIFX_LABEL_SIZE,
+         bulb->state.label,
+-        lgtd_addrtoa(bulb->addr),
++        LGTD_IEEE8023MACTOA(bulb->addr, addr),
+         bulb->gw->ip_addr,
+         bulb->gw->port
+     );
+diff --git a/lifx/gateway.c b/lifx/gateway.c
+--- a/lifx/gateway.c
++++ b/lifx/gateway.c
+@@ -76,9 +76,10 @@
+         lgtd_lifx_gateway_remove_and_close_bulb(gw, bulb);
+     }
+ 
++    char site[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_info(
+         "connection with gateway bulb [%s]:%hu (site %s) closed",
+-        gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array)
++        gw->ip_addr, gw->port, LGTD_IEEE8023MACTOA(gw->site.as_array, site)
+     );
+     free(gw);
+ }
+@@ -197,9 +198,10 @@
+         gw, pkt_type, pkt, &pkt_info
+     );
+ 
++    char site[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_debug(
+         "sending %s to site %s",
+-        pkt_info->name, lgtd_addrtoa(gw->site.as_array)
++        pkt_info->name, LGTD_IEEE8023MACTOA(gw->site.as_array, site)
+     );
+ 
+     return rv; // FIXME, have real return values on the send paths...
+@@ -215,9 +217,10 @@
+         gw, pkt_type, pkt, &pkt_info
+     );
+ 
++    char site[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_info(
+         "sending %s to site %s",
+-        pkt_info->name, lgtd_addrtoa(gw->site.as_array)
++        pkt_info->name, LGTD_IEEE8023MACTOA(gw->site.as_array, site)
+     );
+ 
+     return rv; // FIXME, have real return values on the send paths...
+@@ -266,9 +269,10 @@
+         bulb = lgtd_lifx_bulb_open(gw, bulb_addr);
+         if (bulb) {
+             SLIST_INSERT_HEAD(&gw->bulbs, bulb, link_by_gw);
++            char addr[LGTD_LIFX_ADDR_STRLEN];
+             lgtd_info(
+                 "bulb %s on [%s]:%hu",
+-                lgtd_addrtoa(bulb_addr), gw->ip_addr, gw->port
++                LGTD_IEEE8023MACTOA(bulb->addr, addr), gw->ip_addr, gw->port
+             );
+         }
+     }
+@@ -328,9 +332,11 @@
+         goto error_allocate;
+     }
+ 
++    char site_addr[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_info(
+         "gateway for site %s at [%s]:%hu",
+-        lgtd_addrtoa(gw->site.as_array), gw->ip_addr, gw->port
++        LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr),
++        gw->ip_addr, gw->port
+     );
+     LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link);
+ 
+@@ -444,10 +450,12 @@
+     LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, removed_tags) {
+         assert(gw->tag_refcounts[tag_id] > 0);
+         if (--gw->tag_refcounts[tag_id] == 0) {
++            char site[LGTD_LIFX_ADDR_STRLEN];
+             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)
++                gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL,
++                tag_id, gw->ip_addr, gw->port,
++                LGTD_IEEE8023MACTOA(gw->site.as_array, site)
+             );
+             struct lgtd_lifx_packet_tag_labels pkt = {
+                 .tags = ~(gw->tag_ids & ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))
+@@ -465,10 +473,12 @@
+ {
+     assert(gw && hdr && pkt);
+ 
++    char addr[LGTD_LIFX_ADDR_STRLEN], site[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_debug(
+         "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
++        gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
++        LGTD_IEEE8023MACTOA(hdr->site, site), pkt->service_type
+     );
+ }
+ 
+@@ -479,11 +489,13 @@
+ {
+     assert(gw && hdr && pkt);
+ 
++    char addr[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_debug(
+         "SET_LIGHT_STATE <-- [%s]:%hu - %s "
+         "hue=%#hx, saturation=%#hx, brightness=%#hx, "
+         "kelvin=%d, dim=%#hx, power=%#hx, label=%.*s, tags=%#jx",
+-        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
++        gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
+         pkt->hue, pkt->saturation, pkt->brightness, pkt->kelvin,
+         pkt->dim, pkt->power, LGTD_LIFX_LABEL_SIZE, pkt->label,
+         (uintmax_t)pkt->tags
+@@ -554,9 +566,11 @@
+ {
+     assert(gw && hdr && pkt);
+ 
++    char addr[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_debug(
+         "SET_POWER_STATE <-- [%s]:%hu - %s power=%#hx",
+-        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), pkt->power
++        gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), pkt->power
+     );
+ 
+     struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
+@@ -596,13 +610,15 @@
+     assert(tag_id >= -1);
+     assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS);
+ 
++    char site[LGTD_LIFX_ADDR_STRLEN];
++    LGTD_IEEE8023MACTOA(gw->site.as_array, site);
++
+     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)
++                tag_label, gw->ip_addr, gw->port, site
+             );
+             return -1;
+         }
+@@ -614,14 +630,13 @@
+         if (!tag) {
+             lgtd_warn(
+                 "couldn't allocate a new reference to tag [%s] (site %s)",
+-                tag_label, lgtd_addrtoa(gw->site.as_array)
++                tag_label, site
+             );
+             return -1;
+         }
+         lgtd_debug(
+             "tag_id %d allocated for tag [%s] on gw [%s]:%hu (site %s)",
+-            tag_id, tag_label, gw->ip_addr, gw->port,
+-            lgtd_addrtoa(gw->site.as_array)
++            tag_id, tag_label, gw->ip_addr, gw->port, site
+         );
+         gw->tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
+         gw->tags[tag_id] = tag;
+@@ -638,11 +653,12 @@
+     assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS);
+ 
+     if (gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id)) {
++        char site[LGTD_LIFX_ADDR_STRLEN];
+         lgtd_debug(
+             "tag_id %d deallocated for tag [%s] on gw [%s]:%hu (site %s)",
+             tag_id, gw->tags[tag_id]->label,
+             gw->ip_addr, gw->port,
+-            lgtd_addrtoa(gw->site.as_array)
++            LGTD_IEEE8023MACTOA(gw->site.as_array, site)
+         );
+         lgtd_lifx_tagging_decref(gw->tags[tag_id], gw);
+         gw->tag_ids &= ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
+@@ -657,9 +673,11 @@
+ {
+     assert(gw && hdr && pkt);
+ 
++    char addr[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_debug(
+         "SET_TAG_LABELS <-- [%s]:%hu - %s label=%.*s, tags=%jx",
+-        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
++        gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
+         LGTD_LIFX_LABEL_SIZE, pkt->label, (uintmax_t)pkt->tags
+     );
+ 
+@@ -679,9 +697,11 @@
+ {
+     assert(gw && hdr && pkt);
+ 
++    char addr[LGTD_LIFX_ADDR_STRLEN];
+     lgtd_debug(
+         "SET_TAGS <-- [%s]:%hu - %s tags=%#jx",
+-        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
++        gw->ip_addr, gw->port,
++        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
+         (uintmax_t)pkt->tags
+     );
+ 
+@@ -692,6 +712,7 @@
+         return;
+     }
+ 
++    char bulb_addr[LGTD_LIFX_ADDR_STRLEN], site_addr[LGTD_LIFX_ADDR_STRLEN];
+     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))) {
+@@ -699,8 +720,9 @@
+                 "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_IEEE8023MACTOA(b->addr, bulb_addr),
++                LGTD_LIFX_LABEL_SIZE, b->state.label, gw->ip_addr, gw->port,
++                LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr)
+             );
+         }
+     }
+diff --git a/lifx/tagging.c b/lifx/tagging.c
+--- a/lifx/tagging.c
++++ b/lifx/tagging.c
+@@ -123,10 +123,11 @@
+         if (dealloc_tag) {
+             lgtd_info("discovered tag [%s]", tag_label);
+         }
++        char site_addr[LGTD_LIFX_ADDR_STRLEN];
+         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
++            LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr), tag_id
+         );
+         site->gw = gw;
+         site->tag_id = tag_id;
+@@ -147,10 +148,11 @@
+     struct lgtd_lifx_site *site;
+     site = lgtd_lifx_tagging_find_site(&tag->sites, gw);
+     if (site) {
++        char site_addr[LGTD_LIFX_ADDR_STRLEN];
+         lgtd_debug(
+             "tag [%s] removed from gw [%s]:%hu (site %s)",
+             tag->label, gw->ip_addr, gw->port,
+-            lgtd_addrtoa(gw->site.as_array)
++            LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr)
+         );
+         LIST_REMOVE(site, link);
+         free(site);
+diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
+--- a/lifx/wire_proto.h
++++ b/lifx/wire_proto.h
+@@ -44,6 +44,7 @@
+ enum { LGTD_LIFX_PROTOCOL_PORT = 56700 };
+ 
+ enum { LGTD_LIFX_ADDR_LENGTH = 6 };
++enum { LGTD_LIFX_ADDR_STRLEN = 32 };
+ 
+ #pragma pack(push, 1)
+ 
+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
+@@ -31,10 +31,12 @@
+     }
+ 
+     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 1, 2, 3, 4, 5 };
++    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
+     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)
++            LGTD_IEEE8023MACTOA(bulb->addr, addr),
++            LGTD_IEEE8023MACTOA(expected_addr, expected)
+         );
+     }
+ 
+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
+@@ -33,10 +33,12 @@
+     }
+ 
+     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 };
++    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
+     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)
++            LGTD_IEEE8023MACTOA(bulb->addr, addr),
++            LGTD_IEEE8023MACTOA(expected_addr, expected)
+         );
+     }
+ 
+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
+@@ -101,10 +101,12 @@
+     }
+ 
+     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 };
++    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
+     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)
++            LGTD_IEEE8023MACTOA(bulb->addr, addr),
++            LGTD_IEEE8023MACTOA(expected_addr, expected)
+         );
+     }
+ 
+diff --git a/tests/lifx/bulb/test_bulb_open.c b/tests/lifx/bulb/test_bulb_open.c
+--- a/tests/lifx/bulb/test_bulb_open.c
++++ b/tests/lifx/bulb/test_bulb_open.c
+@@ -14,10 +14,12 @@
+         errx(1, "lgtd_lifx_bulb_open didn't return any bulb");
+     }
+ 
++    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
+     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)
++            LGTD_IEEE8023MACTOA(bulb->addr, addr),
++            LGTD_IEEE8023MACTOA(bulb_addr, expected)
+         );
+     }
+ 
+diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
+--- a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
++++ b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
+@@ -78,10 +78,13 @@
+     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = {
+         0, 0, 44, 0, 0, 0
+     };
++    char expected_addr_buf[LGTD_LIFX_ADDR_STRLEN];
++    char dev_addr[LGTD_LIFX_ADDR_STRLEN];
+     if (memcmp(hdr.target.device_addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) {
+         lgtd_errx(
+             1, "device addr = %s (expected = %s)",
+-            lgtd_addrtoa(hdr.target.device_addr), lgtd_addrtoa(expected_addr)
++            LGTD_IEEE8023MACTOA(hdr.target.device_addr, dev_addr),
++            LGTD_IEEE8023MACTOA(expected_addr, expected_addr_buf)
+         );
+     }
+     if (le16toh(hdr.packet_type) != LGTD_LIFX_ECHO_REQUEST) {
+@@ -109,7 +112,8 @@
+     if (memcmp(hdr.target.device_addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) {
+         lgtd_errx(
+             1, "device addr = %s (expected = %s)",
+-            lgtd_addrtoa(hdr.target.device_addr), lgtd_addrtoa(expected_addr)
++            LGTD_IEEE8023MACTOA(hdr.target.device_addr, dev_addr),
++            LGTD_IEEE8023MACTOA(expected_addr, expected_addr_buf)
+         );
+     }
+     if (hdr.size != 42) {
--- a/fix_sockaddrtoa_mess.patch	Sat Aug 15 23:22:19 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,634 +0,0 @@
-# HG changeset patch
-# Parent  44e0729fda3081537c2615a458b8d403867ca9b4
-
-diff --git a/core/lightsd.h b/core/lightsd.h
---- a/core/lightsd.h
-+++ b/core/lightsd.h
-@@ -74,14 +74,15 @@
- extern struct lgtd_opts lgtd_opts;
- extern struct event_base *lgtd_ev_base;
- 
--const char *lgtd_addrtoa(const uint8_t *);
-+char *lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen);
-+#define LGTD_IEEE8023MACTOA(addr, buf) \
-+    lgtd_iee8023mactoa((addr), (buf), sizeof(buf))
- void lgtd_sockaddrtoa(const struct sockaddr_storage *, char *buf, int buflen);
- short lgtd_sockaddrport(const struct sockaddr_storage *);
- 
--void lgtd_print_duration(uint64_t, char *, int);
--#define LGTD_PRINT_DURATION(secs, arr) do {             \
--    lgtd_print_duration((secs), (arr), sizeof((arr)));  \
--} while (0)
-+char *lgtd_print_duration(uint64_t, char *, int);
-+#define LGTD_PRINT_DURATION(secs, arr) \
-+    lgtd_print_duration((secs), (arr), sizeof((arr)))
- 
- void _lgtd_err(void (*)(int, const char *, ...), int, const char *, ...)
-     __attribute__((format(printf, 3, 4)));
-diff --git a/core/log.c b/core/log.c
---- a/core/log.c
-+++ b/core/log.c
-@@ -74,17 +74,18 @@
-     fprintf(stderr, "[%s] %s", loglvl, showprogname ? "lightsd: " : "");
- }
- 
--const char *
--lgtd_addrtoa(const uint8_t *addr)
-+char *
-+lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen)
- {
-     assert(addr);
-+    assert(buf);
-+    assert(buflen >= 2 * 6 + 5 + 1);
- 
--    static char str[LGTD_LIFX_ADDR_LENGTH * 2 + LGTD_LIFX_ADDR_LENGTH - 1 + 1];
-     snprintf(
--        str, sizeof(str), "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
-+        buf, buflen, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
-         addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]
-     );
--    return str;
-+    return buf;
- }
- 
- void
-@@ -113,7 +114,7 @@
-     }
- }
- 
--void
-+char *
- lgtd_print_duration(uint64_t secs, char *buf, int bufsz)
- {
-     assert(buf);
-@@ -131,6 +132,7 @@
-         i = LGTD_MIN(i + n, bufsz);
-     }
-     snprintf(&buf[i], bufsz - i, "%02d:%02d", hours, minutes);
-+    return buf;
- }
- 
- void
-diff --git a/core/proto.c b/core/proto.c
---- a/core/proto.c
-+++ b/core/proto.c
-@@ -201,9 +201,16 @@
-     SLIST_FOREACH(device, devices, link) {
-         struct lgtd_lifx_bulb *bulb = device->device;
- 
--        char buf[2048];
-+        char buf[2048],
-+             gw_addr[LGTD_LIFX_ADDR_STRLEN],
-+             site_addr[LGTD_LIFX_ADDR_STRLEN],
-+             bulb_addr[LGTD_LIFX_ADDR_STRLEN];
-         int i = 0;
- 
-+        LGTD_IEEE8023MACTOA(bulb->addr, bulb_addr);
-+        LGTD_IEEE8023MACTOA(bulb->gw->addr, gw_addr);
-+        LGTD_IEEE8023MACTOA(bulb->gw->site.as_array, site_addr);
-+
-         LGTD_SNPRINTF_APPEND(
-             buf, i, (int)sizeof(buf),
-             "{"
-@@ -215,9 +222,7 @@
-                         "\"url\":\"tcp://[%s]:%hu\","
-                         "\"latency\":%ju"
-                     "},",
--            lgtd_addrtoa(bulb->addr),
--            lgtd_addrtoa(bulb->gw->addr),
--            lgtd_addrtoa(bulb->gw->site.as_array),
-+            bulb_addr, gw_addr, site_addr,
-             bulb->gw->ip_addr, bulb->gw->port,
-             (uintmax_t)LGTD_LIFX_GATEWAY_LATENCY(bulb->gw)
-         );
-@@ -299,15 +304,14 @@
-             "\"tags\":[",
-             h, s, b, bulb->state.kelvin,
-             bulb->state.power == LGTD_LIFX_POWER_ON ? "true" : "false",
--            bulb->state.label[0] ? bulb->state.label : lgtd_addrtoa(bulb->addr)
-+            bulb->state.label[0] ? bulb->state.label : bulb_addr
-         );
- 
-         if (i >= (int)sizeof(buf)) {
-             lgtd_warnx(
-                 "can't send state of bulb %s (%s) to client "
-                 "[%s]:%hu: output buffer to small",
--                bulb->state.label, lgtd_addrtoa(bulb->addr),
--                client->ip_addr, client->port
-+                bulb->state.label, bulb_addr, client->ip_addr, client->port
-             );
-             continue;
-         }
-@@ -326,8 +330,7 @@
-                     "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)
-+                    bulb_addr, bulb->gw->ip_addr, bulb->gw->port, site_addr
-                 );
-             }
-         }
-diff --git a/core/router.c b/core/router.c
---- a/core/router.c
-+++ b/core/router.c
-@@ -106,7 +106,9 @@
-         bulb->expected_power_on = payload->power;
-     }
- 
--    lgtd_info("sending %s to %s", pkt_info->name, lgtd_addrtoa(bulb->addr));
-+    char addr[LGTD_LIFX_ADDR_STRLEN];
-+    LGTD_IEEE8023MACTOA(bulb->addr, addr);
-+    lgtd_info("sending %s to %s", pkt_info->name, addr);
- }
- 
- void
-diff --git a/lifx/bulb.c b/lifx/bulb.c
---- a/lifx/bulb.c
-+++ b/lifx/bulb.c
-@@ -153,11 +153,12 @@
-         LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(bulbs_powered_on, -1);
-     }
-     RB_REMOVE(lgtd_lifx_bulb_map, &lgtd_lifx_bulbs_table, bulb);
-+    char addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_info(
-         "closed bulb \"%.*s\" (%s) on [%s]:%hu",
-         LGTD_LIFX_LABEL_SIZE,
-         bulb->state.label,
--        lgtd_addrtoa(bulb->addr),
-+        LGTD_IEEE8023MACTOA(bulb->addr, addr),
-         bulb->gw->ip_addr,
-         bulb->gw->port
-     );
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -79,9 +79,10 @@
-         lgtd_lifx_gateway_remove_and_close_bulb(gw, bulb);
-     }
- 
-+    char site[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_info(
-         "connection with gateway bulb [%s]:%hu (site %s) closed",
--        gw->ip_addr, gw->port, lgtd_addrtoa(gw->site.as_array)
-+        gw->ip_addr, gw->port, LGTD_IEEE8023MACTOA(gw->site.as_array, site)
-     );
-     free(gw);
- }
-@@ -200,9 +201,10 @@
-         gw, pkt_type, pkt, &pkt_info
-     );
- 
-+    char site[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "sending %s to site %s",
--        pkt_info->name, lgtd_addrtoa(gw->site.as_array)
-+        pkt_info->name, LGTD_IEEE8023MACTOA(gw->site.as_array, site)
-     );
- 
-     return rv; // FIXME, have real return values on the send paths...
-@@ -218,9 +220,10 @@
-         gw, pkt_type, pkt, &pkt_info
-     );
- 
-+    char site[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_info(
-         "sending %s to site %s",
--        pkt_info->name, lgtd_addrtoa(gw->site.as_array)
-+        pkt_info->name, LGTD_IEEE8023MACTOA(gw->site.as_array, site)
-     );
- 
-     return rv; // FIXME, have real return values on the send paths...
-@@ -269,9 +272,10 @@
-         bulb = lgtd_lifx_bulb_open(gw, bulb_addr);
-         if (bulb) {
-             SLIST_INSERT_HEAD(&gw->bulbs, bulb, link_by_gw);
-+            char addr[LGTD_LIFX_ADDR_STRLEN];
-             lgtd_info(
-                 "bulb %s on [%s]:%hu",
--                lgtd_addrtoa(bulb_addr), gw->ip_addr, gw->port
-+                LGTD_IEEE8023MACTOA(bulb->addr, addr), gw->ip_addr, gw->port
-             );
-         }
-     }
-@@ -362,9 +366,11 @@
-         goto error_allocate;
-     }
- 
-+    char site_addr[LGTD_LIFX_ADDR_STRLEN], addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_info(
--        "gateway for site %s at [%s]:%hu",
--        lgtd_addrtoa(gw->site.as_array), gw->ip_addr, gw->port
-+        "gateway for site %s at [%s]:%hu (%s)",
-+        LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr),
-+        gw->ip_addr, gw->port, LGTD_IEEE8023MACTOA(gw->addr, addr)
-     );
-     LIST_INSERT_HEAD(&lgtd_lifx_gateways, gw, link);
- 
-@@ -478,10 +484,12 @@
-     LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, removed_tags) {
-         assert(gw->tag_refcounts[tag_id] > 0);
-         if (--gw->tag_refcounts[tag_id] == 0) {
-+            char site[LGTD_LIFX_ADDR_STRLEN];
-             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)
-+                gw->tags[tag_id] ? gw->tags[tag_id]->label : NULL,
-+                tag_id, gw->ip_addr, gw->port,
-+                LGTD_IEEE8023MACTOA(gw->site.as_array, site)
-             );
-             struct lgtd_lifx_packet_tag_labels pkt = {
-                 .tags = ~(gw->tag_ids & ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id))
-@@ -499,10 +507,12 @@
- {
-     assert(gw && hdr && pkt);
- 
-+    char addr[LGTD_LIFX_ADDR_STRLEN], site[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "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
-+        gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-+        LGTD_IEEE8023MACTOA(hdr->site, site), pkt->service_type
-     );
- }
- 
-@@ -513,11 +523,13 @@
- {
-     assert(gw && hdr && pkt);
- 
-+    char addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "SET_LIGHT_STATE <-- [%s]:%hu - %s "
-         "hue=%#hx, saturation=%#hx, brightness=%#hx, "
-         "kelvin=%d, dim=%#hx, power=%#hx, label=%.*s, tags=%#jx",
--        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-         pkt->hue, pkt->saturation, pkt->brightness, pkt->kelvin,
-         pkt->dim, pkt->power, LGTD_LIFX_LABEL_SIZE, pkt->label,
-         (uintmax_t)pkt->tags
-@@ -584,9 +596,11 @@
- {
-     assert(gw && hdr && pkt);
- 
-+    char addr[LGTD_LIFX_ADDR_LENGTH];
-     lgtd_debug(
-         "SET_POWER_STATE <-- [%s]:%hu - %s power=%#hx",
--        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), pkt->power
-+        gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr), pkt->power
-     );
- 
-     LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-@@ -621,13 +635,15 @@
-     assert(tag_id >= -1);
-     assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS);
- 
-+    char site[LGTD_LIFX_ADDR_STRLEN];
-+    LGTD_IEEE8023MACTOA(gw->site.as_array, site);
-+
-     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)
-+                tag_label, gw->ip_addr, gw->port, site
-             );
-             return -1;
-         }
-@@ -639,14 +655,13 @@
-         if (!tag) {
-             lgtd_warn(
-                 "couldn't allocate a new reference to tag [%s] (site %s)",
--                tag_label, lgtd_addrtoa(gw->site.as_array)
-+                tag_label, site
-             );
-             return -1;
-         }
-         lgtd_debug(
-             "tag_id %d allocated for tag [%s] on gw [%s]:%hu (site %s)",
--            tag_id, tag_label, gw->ip_addr, gw->port,
--            lgtd_addrtoa(gw->site.as_array)
-+            tag_id, tag_label, gw->ip_addr, gw->port, site
-         );
-         gw->tag_ids |= LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-         gw->tags[tag_id] = tag;
-@@ -663,11 +678,12 @@
-     assert(tag_id < LGTD_LIFX_GATEWAY_MAX_TAGS);
- 
-     if (gw->tag_ids & LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id)) {
-+        char site[LGTD_LIFX_ADDR_STRLEN];
-         lgtd_debug(
-             "tag_id %d deallocated for tag [%s] on gw [%s]:%hu (site %s)",
-             tag_id, gw->tags[tag_id]->label,
-             gw->ip_addr, gw->port,
--            lgtd_addrtoa(gw->site.as_array)
-+            LGTD_IEEE8023MACTOA(gw->site.as_array, site)
-         );
-         lgtd_lifx_tagging_decref(gw->tags[tag_id], gw);
-         gw->tag_ids &= ~LGTD_LIFX_WIRE_TAG_ID_TO_VALUE(tag_id);
-@@ -682,9 +698,11 @@
- {
-     assert(gw && hdr && pkt);
- 
-+    char addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "SET_TAG_LABELS <-- [%s]:%hu - %s label=%.*s, tags=%jx",
--        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-         LGTD_LIFX_LABEL_SIZE, pkt->label, (uintmax_t)pkt->tags
-     );
- 
-@@ -705,15 +723,18 @@
- {
-     assert(gw && hdr && pkt);
- 
-+    char addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "SET_TAGS <-- [%s]:%hu - %s tags=%#jx",
--        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-         (uintmax_t)pkt->tags
-     );
- 
-     struct lgtd_lifx_bulb *b;
-     LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, hdr->target.device_addr);
- 
-+    char bulb_addr[LGTD_LIFX_ADDR_STRLEN], site_addr[LGTD_LIFX_ADDR_STRLEN];
-     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))) {
-@@ -721,8 +742,9 @@
-                 "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_IEEE8023MACTOA(b->addr, bulb_addr),
-+                LGTD_LIFX_LABEL_SIZE, b->state.label, gw->ip_addr, gw->port,
-+                LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr)
-             );
-         }
-     }
-@@ -756,10 +778,12 @@
-         return;
-     }
- 
-+    char addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "%s <-- [%s]:%hu - %s "
-         "signal_strength=%f, rx_bytes=%u, tx_bytes=%u, temperature=%hu",
--        type, gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        type, gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-         pkt->signal_strength, pkt->rx_bytes, pkt->tx_bytes, pkt->temperature
-     );
- 
-@@ -776,10 +800,6 @@
- {
-     assert(gw && hdr && pkt);
- 
--    char built_at[64], installed_at[64];
--    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->built_at, built_at);
--    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->installed_at, installed_at);
--
-     const char  *type;
-     enum lgtd_lifx_bulb_ips ip_id;
-     switch (hdr->packet_type) {
-@@ -799,11 +819,15 @@
-         return;
-     }
- 
-+    char built_at[64], installed_at[64], addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "%s <-- [%s]:%hu - %s "
-         "built_at=%s, installed_at=%s, version=%u",
--        type, gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
--        built_at, installed_at, pkt->version
-+        type, gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-+        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->built_at, built_at),
-+        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->installed_at, installed_at),
-+        pkt->version
-     );
- 
-     LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-@@ -819,10 +843,12 @@
- {
-     assert(gw && hdr && pkt);
- 
-+    char addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "PRODUCT_INFO <-- [%s]:%hu - %s "
-         "vendor_id=%#x, product_id=%#x, version=%u",
--        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-         pkt->vendor_id, pkt->product_id, pkt->version
-     );
- 
-@@ -839,15 +865,14 @@
- {
-     assert(gw && hdr && pkt);
- 
--    char device_time[64], uptime[64], downtime[64];
--    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->time, device_time);
--    LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->uptime), uptime);
--    LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->downtime), downtime);
--
-+    char device_time[64], uptime[64], downtime[64], addr[LGTD_LIFX_ADDR_STRLEN];
-     lgtd_debug(
-         "PRODUCT_INFO <-- [%s]:%hu - %s time=%s, uptime=%s, downtime=%s",
--        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
--        device_time, uptime, downtime
-+        gw->ip_addr, gw->port,
-+        LGTD_IEEE8023MACTOA(hdr->target.device_addr, addr),
-+        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->time, device_time),
-+        LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->uptime), uptime),
-+        LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->downtime), downtime)
-     );
- 
-     LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-diff --git a/lifx/tagging.c b/lifx/tagging.c
---- a/lifx/tagging.c
-+++ b/lifx/tagging.c
-@@ -123,10 +123,11 @@
-         if (dealloc_tag) {
-             lgtd_info("discovered tag [%s]", tag_label);
-         }
-+        char site_addr[LGTD_LIFX_ADDR_STRLEN];
-         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
-+            LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr), tag_id
-         );
-         site->gw = gw;
-         site->tag_id = tag_id;
-@@ -147,10 +148,11 @@
-     struct lgtd_lifx_site *site;
-     site = lgtd_lifx_tagging_find_site(&tag->sites, gw);
-     if (site) {
-+        char site_addr[LGTD_LIFX_ADDR_STRLEN];
-         lgtd_debug(
-             "tag [%s] removed from gw [%s]:%hu (site %s)",
-             tag->label, gw->ip_addr, gw->port,
--            lgtd_addrtoa(gw->site.as_array)
-+            LGTD_IEEE8023MACTOA(gw->site.as_array, site_addr)
-         );
-         LIST_REMOVE(site, link);
-         free(site);
-diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -465,7 +465,7 @@
-     return LGTD_LIFX_WAVEFORM_INVALID;
- }
- 
--void
-+char *
- lgtd_lifx_wire_print_nsec_timestamp(uint64_t nsec_ts, char *buf, int bufsz)
- {
-     assert(buf);
-@@ -477,10 +477,11 @@
-     if (gmtime_r(&ts, &tm_utc)) {
-         int64_t usecs = LGTD_NSECS_TO_USECS(nsec_ts - LGTD_SECS_TO_NSECS(ts));
-         LGTD_TM_TO_ISOTIME(&tm_utc, buf, bufsz, usecs);
--        return;
-+    } else {
-+        buf[0] = '\0';
-     }
- 
--    buf[0] = '\0';
-+    return buf;
- }
- 
- static void
-diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
---- a/lifx/wire_proto.h
-+++ b/lifx/wire_proto.h
-@@ -44,6 +44,7 @@
- enum { LGTD_LIFX_PROTOCOL_PORT = 56700 };
- 
- enum { LGTD_LIFX_ADDR_LENGTH = 6 };
-+enum { LGTD_LIFX_ADDR_STRLEN = 32 };
- 
- #pragma pack(push, 1)
- 
-@@ -355,10 +356,9 @@
-          (tag_id_varname) = lgtd_lifx_wire_next_tag_id((tag_id_varname), (tags)))
- 
- enum lgtd_lifx_waveform_type lgtd_lifx_wire_waveform_string_id_to_type(const char *, int);
--void lgtd_lifx_wire_print_nsec_timestamp(uint64_t, char *, int);
--#define LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(ts, arr) do {               \
--    lgtd_lifx_wire_print_nsec_timestamp((ts), (arr), sizeof((arr)));    \
--} while (0)
-+char* lgtd_lifx_wire_print_nsec_timestamp(uint64_t, char *, int);
-+#define LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(ts, arr) \
-+    lgtd_lifx_wire_print_nsec_timestamp((ts), (arr), sizeof((arr)))
- 
- const struct lgtd_lifx_packet_info *lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type);
- void lgtd_lifx_wire_load_packet_info_map(void);
-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
-@@ -33,10 +33,12 @@
-     }
- 
-     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 1, 2, 3, 4, 5 };
-+    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
-     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)
-+            LGTD_IEEE8023MACTOA(bulb->addr, addr),
-+            LGTD_IEEE8023MACTOA(expected_addr, expected)
-         );
-     }
- 
-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
-@@ -35,10 +35,12 @@
-     }
- 
-     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 };
-+    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
-     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)
-+            LGTD_IEEE8023MACTOA(bulb->addr, addr),
-+            LGTD_IEEE8023MACTOA(expected_addr, expected)
-         );
-     }
- 
-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
-@@ -103,10 +103,12 @@
-     }
- 
-     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = { 5, 4, 3, 2, 1 };
-+    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
-     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)
-+            LGTD_IEEE8023MACTOA(bulb->addr, addr),
-+            LGTD_IEEE8023MACTOA(expected_addr, expected)
-         );
-     }
- 
-diff --git a/tests/lifx/bulb/test_bulb_open.c b/tests/lifx/bulb/test_bulb_open.c
---- a/tests/lifx/bulb/test_bulb_open.c
-+++ b/tests/lifx/bulb/test_bulb_open.c
-@@ -16,10 +16,12 @@
-         errx(1, "lgtd_lifx_bulb_open didn't return any bulb");
-     }
- 
-+    char addr[LGTD_LIFX_ADDR_STRLEN], expected[LGTD_LIFX_ADDR_STRLEN];
-     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)
-+            LGTD_IEEE8023MACTOA(bulb->addr, addr),
-+            LGTD_IEEE8023MACTOA(bulb_addr, expected)
-         );
-     }
- 
-diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
---- a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
-+++ b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
-@@ -78,10 +78,13 @@
-     uint8_t expected_addr[LGTD_LIFX_ADDR_LENGTH] = {
-         0, 0, 44, 0, 0, 0
-     };
-+    char expected_addr_buf[LGTD_LIFX_ADDR_STRLEN];
-+    char dev_addr[LGTD_LIFX_ADDR_STRLEN];
-     if (memcmp(hdr.target.device_addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) {
-         lgtd_errx(
-             1, "device addr = %s (expected = %s)",
--            lgtd_addrtoa(hdr.target.device_addr), lgtd_addrtoa(expected_addr)
-+            LGTD_IEEE8023MACTOA(hdr.target.device_addr, dev_addr),
-+            LGTD_IEEE8023MACTOA(expected_addr, expected_addr_buf)
-         );
-     }
-     if (le16toh(hdr.packet_type) != LGTD_LIFX_ECHO_REQUEST) {
-@@ -109,7 +112,8 @@
-     if (memcmp(hdr.target.device_addr, expected_addr, LGTD_LIFX_ADDR_LENGTH)) {
-         lgtd_errx(
-             1, "device addr = %s (expected = %s)",
--            lgtd_addrtoa(hdr.target.device_addr), lgtd_addrtoa(expected_addr)
-+            LGTD_IEEE8023MACTOA(hdr.target.device_addr, dev_addr),
-+            LGTD_IEEE8023MACTOA(expected_addr, expected_addr_buf)
-         );
-     }
-     if (hdr.size != 42) {
--- a/implement_some_metadata_packet_types.patch	Sat Aug 15 23:22:19 2015 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1385 +0,0 @@
-# HG changeset patch
-# Parent  de1c6a01d9955785187b591d15ab41b516c7cb78
-Handle various informational packet types
-
-- MCU state & firmware info;
-- WiFi state & firmware info;
-- device/runtime info.
-
-diff --git a/core/daemon.c b/core/daemon.c
---- a/core/daemon.c
-+++ b/core/daemon.c
-@@ -106,10 +106,9 @@
-     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 TITLE_APPEND(fmt, ...) LGTD_SNPRINTF_APPEND(    \
-+    title, i, (int)sizeof(title), (fmt), __VA_ARGS__    \
-+)
- 
- #define PREFIX(fmt, ...) TITLE_APPEND(                              \
-     "%s" fmt, (i && title[i - 1] == ')' ? "; " : ""), __VA_ARGS__   \
-diff --git a/core/lightsd.h b/core/lightsd.h
---- a/core/lightsd.h
-+++ b/core/lightsd.h
-@@ -24,10 +24,33 @@
- #define LGTD_ABS(v) ((v) >= 0 ? (v) : (v) * -1)
- #define LGTD_MIN(a, b) ((a) < (b) ? (a) : (b))
- #define LGTD_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
--#define LGTD_MSECS_TO_TIMEVAL(v) { \
-+#define LGTD_MSECS_TO_TIMEVAL(v) {  \
-     .tv_sec = (v) / 1000,           \
-     .tv_usec = ((v) % 1000) * 1000  \
- }
-+#define LGTD_NSECS_TO_USECS(v) ((v) / (unsigned int)1E6)
-+#define LGTD_NSECS_TO_SECS(v) ((v) / (unsigned int)1E9)
-+#define LGTD_SECS_TO_NSECS(v) ((v) * (unsigned int)1E9)
-+#define LGTD_TM_TO_ISOTIME(tm, sbuf, bufsz, usec) do {                          \
-+    /* '2015-01-02T10:13:16.132222+00:00' */                                    \
-+    if ((usec)) {                                                               \
-+        snprintf(                                                               \
-+            (sbuf), (bufsz), "%d-%02d-%02dT%02d:%02d:%02d.%jd%c%02ld:%02ld",    \
-+            1900 + (tm)->tm_year, 1 + (tm)->tm_mon, (tm)->tm_mday,              \
-+            (tm)->tm_hour, (tm)->tm_min, (tm)->tm_sec, (intmax_t)usec,          \
-+            (tm)->tm_gmtoff >= 0 ? '+' : '-', /* %+02ld doesn't work */         \
-+            LGTD_ABS((tm)->tm_gmtoff / 60 / 60), (tm)->tm_gmtoff % (60 * 60)    \
-+        );                                                                      \
-+    } else {                                                                    \
-+        snprintf(                                                               \
-+            (sbuf), (bufsz), "%d-%02d-%02dT%02d:%02d:%02d%c%02ld:%02ld",        \
-+            1900 + (tm)->tm_year, 1 + (tm)->tm_mon, (tm)->tm_mday,              \
-+            (tm)->tm_hour, (tm)->tm_min, (tm)->tm_sec,                          \
-+            (tm)->tm_gmtoff >= 0 ? '+' : '-', /* %+02ld doesn't work */         \
-+            LGTD_ABS((tm)->tm_gmtoff / 60 / 60), (tm)->tm_gmtoff % (60 * 60)    \
-+        );                                                                      \
-+    }                                                                           \
-+} while (0)
- #define LGTD_SNPRINTF_APPEND(buf, i, bufsz, ...) do {       \
-     int n = snprintf(&(buf)[(i)], bufsz - i, __VA_ARGS__);  \
-     (i) = LGTD_MIN((i) + n, bufsz);                         \
-@@ -55,6 +78,11 @@
- void lgtd_sockaddrtoa(const struct sockaddr_storage *, char *buf, int buflen);
- short lgtd_sockaddrport(const struct sockaddr_storage *);
- 
-+void lgtd_print_duration(uint64_t, char *, int);
-+#define LGTD_PRINT_DURATION(secs, arr) do {             \
-+    lgtd_print_duration((secs), (arr), sizeof((arr)));  \
-+} while (0)
-+
- void _lgtd_err(void (*)(int, const char *, ...), int, const char *, ...)
-     __attribute__((format(printf, 3, 4)));
- #define lgtd_err(eval, fmt, ...) _lgtd_err(err, (eval), (fmt), ##__VA_ARGS__);
-diff --git a/core/log.c b/core/log.c
---- a/core/log.c
-+++ b/core/log.c
-@@ -53,14 +53,7 @@
-     if (!localtime_r(&now.tv_sec, &tm_now)) {
-         goto error;
-     }
--    // '2015-01-02T10:13:16.132222+00:00'
--    snprintf(
--        strbuf, bufsz, "%d-%02d-%02dT%02d:%02d:%02d.%jd%c%02ld:%02ld",
--        1900 + tm_now.tm_year, 1 + tm_now.tm_mon, tm_now.tm_mday,
--        tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec,
--        (intmax_t)now.tv_usec, tm_now.tm_gmtoff >= 0 ? '+' : '-', // %+02ld doesn't work
--        LGTD_ABS(tm_now.tm_gmtoff / 60 / 60), tm_now.tm_gmtoff % (60 * 60)
--    );
-+    LGTD_TM_TO_ISOTIME(&tm_now, strbuf, bufsz, now.tv_usec);
-     return;
- error:
-     strbuf[0] = '\0';
-@@ -121,6 +114,26 @@
- }
- 
- void
-+lgtd_print_duration(uint64_t secs, char *buf, int bufsz)
-+{
-+    assert(buf);
-+    assert(bufsz > 0);
-+
-+    int days = secs / (60 * 60 * 24);
-+    int minutes = secs / 60;
-+    int hours = minutes / 60;
-+    hours = hours % 24;
-+    minutes = minutes % 60;
-+
-+    int i = 0;
-+    if (days) {
-+        int n = snprintf(buf, bufsz, "%d days ", days);
-+        i = LGTD_MIN(i + n, bufsz);
-+    }
-+    snprintf(&buf[i], bufsz - i, "%02d:%02d", hours, minutes);
-+}
-+
-+void
- _lgtd_err(void (*errfn)(int, const char *, ...),
-            int eval,
-            const char *fmt,
-diff --git a/core/proto.c b/core/proto.c
---- a/core/proto.c
-+++ b/core/proto.c
-@@ -195,36 +195,114 @@
-         return;
-     }
- 
--    static const char *state_fmt = ("{"
--        "\"hsbk\":[%s,%s,%s,%hu],"
--        "\"power\":%s,"
--        "\"label\":\"%s\","
--        "\"tags\":[");
-+    lgtd_client_start_send_response(client);
-+    lgtd_client_write_string(client, "[");
-+    struct lgtd_router_device *device;
-+    SLIST_FOREACH(device, devices, link) {
-+        struct lgtd_lifx_bulb *bulb = device->device;
-+
-+        char buf[2048];
-+        int i = 0;
-+
-+        LGTD_SNPRINTF_APPEND(
-+            buf, i, (int)sizeof(buf),
-+            "{"
-+                "\"_lifx\":{"
-+                    "\"addr\": \"%s\","
-+                    "\"gateway\":{"
-+                        "\"addr\":\"%s\","
-+                        "\"site\":\"%s\","
-+                        "\"url\":\"tcp://[%s]:%hu\","
-+                        "\"latency\":%ju"
-+                    "},",
-+            lgtd_addrtoa(bulb->addr),
-+            lgtd_addrtoa(bulb->gw->addr),
-+            lgtd_addrtoa(bulb->gw->site.as_array),
-+            bulb->gw->ip_addr, bulb->gw->port,
-+            (uintmax_t)LGTD_LIFX_GATEWAY_LATENCY(bulb->gw)
-+        );
-+
-+#define PRINT_LIFX_FW_TIMESTAMPS(fw_info, built_at_buf, installed_at_buf)       \
-+    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP((fw_info)->built_at, (built_at_buf));   \
-+    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(                                        \
-+        (fw_info)->installed_at, (installed_at_buf)                             \
-+    )
-+
-+        for (int ip = 0; ip != LGTD_LIFX_BULB_IP_COUNT; ip++) {
-+            char fw_built_at[64], fw_installed_at[64];
-+            PRINT_LIFX_FW_TIMESTAMPS(
-+                &bulb->ips[ip].fw_info, fw_built_at, fw_installed_at
-+            );
-+
-+            LGTD_SNPRINTF_APPEND(
-+                buf, i, (int)sizeof(buf),
-+                "\"%s\":{"
-+                    "\"firmware_built_at\":\"%s\","
-+                    "\"firmware_installed_at\":\"%s\","
-+                    "\"firmware_version\":\"%x\","
-+                    "\"signal_strength\":%f,"
-+                    "\"tx_bytes\":%u,"
-+                    "\"rx_bytes\":%u,"
-+                    "\"temperature\":%u"
-+                "},",
-+                lgtd_lifx_bulb_ip_names[ip],
-+                fw_built_at, fw_installed_at, bulb->ips[ip].fw_info.version,
-+                bulb->ips[ip].state.signal_strength,
-+                bulb->ips[ip].state.tx_bytes,
-+                bulb->ips[ip].state.rx_bytes,
-+                bulb->ips[ip].state.temperature
-+            );
-+        }
-+
-+        LGTD_SNPRINTF_APPEND(
-+            buf, i, (int)sizeof(buf),
-+                "\"product_info\":{"
-+                    "\"vendor_id\":\"%x\","
-+                    "\"product_id\":\"%x\","
-+                    "\"version\":%u"
-+                "},",
-+            bulb->product_info.vendor_id,
-+            bulb->product_info.product_id,
-+            bulb->product_info.version
-+        );
-+
-+        char bulb_time[64];
-+        LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(bulb->runtime_info.time, bulb_time);
-+        LGTD_SNPRINTF_APPEND(
-+            buf, i, (int)sizeof(buf),
-+                "\"runtime_info\":{"
-+                    "\"time\":\"%s\","
-+                    "\"uptime\":%ju,"
-+                    "\"downtime\":%ju"
-+                "}"
-+            "},",
-+            bulb_time,
-+            (uintmax_t)LGTD_NSECS_TO_SECS(bulb->runtime_info.uptime),
-+            (uintmax_t)LGTD_NSECS_TO_SECS(bulb->runtime_info.downtime)
-+        );
- 
- #define PRINT_COMPONENT(src, dst, start, stop)          \
-     lgtd_jsonrpc_uint16_range_to_float_string(          \
-         (src), (start), (stop), (dst), sizeof((dst))    \
-     )
- 
--    lgtd_client_start_send_response(client);
--    lgtd_client_write_string(client, "[");
--    struct lgtd_router_device *device;
--    SLIST_FOREACH(device, devices, link) {
--        struct lgtd_lifx_bulb *bulb = device->device;
--
-         char h[16], s[16], b[16];
-         PRINT_COMPONENT(bulb->state.hue, h, 0, 360);
-         PRINT_COMPONENT(bulb->state.saturation, s, 0, 1);
-         PRINT_COMPONENT(bulb->state.brightness, b, 0, 1);
- 
--        char buf[3072];
--        int written = snprintf(
--            buf, sizeof(buf), state_fmt,
-+        LGTD_SNPRINTF_APPEND(
-+            buf, i, (int)sizeof(buf),
-+            "\"hsbk\":[%s,%s,%s,%hu],"
-+            "\"power\":%s,"
-+            "\"label\":\"%s\","
-+            "\"tags\":[",
-             h, s, b, bulb->state.kelvin,
-             bulb->state.power == LGTD_LIFX_POWER_ON ? "true" : "false",
-             bulb->state.label[0] ? bulb->state.label : lgtd_addrtoa(bulb->addr)
-         );
--        if (written >= (int)sizeof(buf)) {
-+
-+        if (i >= (int)sizeof(buf)) {
-             lgtd_warnx(
-                 "can't send state of bulb %s (%s) to client "
-                 "[%s]:%hu: output buffer to small",
-diff --git a/examples/lightsc.py b/examples/lightsc.py
---- a/examples/lightsc.py
-+++ b/examples/lightsc.py
-@@ -17,7 +17,7 @@
-         "id": str(uuid.uuid4()),
-     }
-     socket.send(json.dumps(payload).encode("utf-8"))
--    response = socket.recv(2048).decode("utf-8")
-+    response = socket.recv(8192).decode("utf-8")
-     try:
-         response = json.loads(response)
-     except ValueError:
-diff --git a/lifx/bulb.c b/lifx/bulb.c
---- a/lifx/bulb.c
-+++ b/lifx/bulb.c
-@@ -33,12 +33,57 @@
- #include "bulb.h"
- #include "gateway.h"
- #include "core/daemon.h"
-+#include "timer.h"
- #include "core/stats.h"
-+#include "core/jsmn.h"
-+#include "core/jsonrpc.h"
-+#include "core/client.h"
-+#include "core/proto.h"
-+#include "core/router.h"
- #include "core/lightsd.h"
- 
- struct lgtd_lifx_bulb_map lgtd_lifx_bulbs_table =
-     RB_INITIALIZER(&lgtd_lifx_bulbs_table);
- 
-+const char * const lgtd_lifx_bulb_ip_names[] = { "mcu", "wifi" };
-+
-+static void
-+lgtd_lifx_bulb_fetch_hardware_info(struct lgtd_lifx_timer *timer,
-+                                   union lgtd_lifx_timer_ctx ctx)
-+{
-+    assert(timer);
-+    assert(ctx.as_uint);
-+
-+    // Get the bulb again, it might have been closed while we were waiting:
-+    const uint8_t *bulb_addr = (const uint8_t *)&ctx.as_uint;
-+    struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_get(bulb_addr);
-+    if (!bulb) {
-+        lgtd_lifx_timer_stop_timer(timer);
-+        return;
-+    }
-+
-+#define RESEND_IF(test, pkt_type) do {                      \
-+    if ((test)) {                                           \
-+        stop = false;                                       \
-+        lgtd_router_send_to_device(bulb, (pkt_type), NULL); \
-+    }                                                       \
-+} while (0)
-+
-+    bool stop = true;
-+    RESEND_IF(!bulb->product_info.vendor_id, LGTD_LIFX_GET_VERSION);
-+    RESEND_IF(
-+        !bulb->ips[LGTD_LIFX_BULB_MCU_IP].fw_info.version,
-+        LGTD_LIFX_GET_MESH_FIRMWARE
-+    );
-+    RESEND_IF(
-+        !bulb->ips[LGTD_LIFX_BULB_WIFI_IP].fw_info.version,
-+        LGTD_LIFX_GET_WIFI_FIRMWARE_STATE
-+    );
-+    if (stop) {
-+        lgtd_lifx_timer_stop_timer(timer);
-+    }
-+}
-+
- struct lgtd_lifx_bulb *
- lgtd_lifx_bulb_get(const uint8_t *addr)
- {
-@@ -68,6 +113,14 @@
- 
-     bulb->last_light_state_at = lgtd_time_monotonic_msecs();
- 
-+    union lgtd_lifx_timer_ctx ctx = { .as_uint = 0 };
-+    memcpy(&ctx.as_uint, addr, LGTD_LIFX_ADDR_LENGTH);
-+    lgtd_lifx_timer_start_timer(
-+        LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS,
-+        lgtd_lifx_bulb_fetch_hardware_info,
-+        ctx
-+    );
-+
-     return bulb;
- }
- 
-@@ -123,6 +176,7 @@
-         LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(
-             bulbs_powered_on, state->power == LGTD_LIFX_POWER_ON ? 1 : -1
-         );
-+        lgtd_router_send_to_device(bulb, LGTD_LIFX_GET_INFO, NULL);
-     }
- 
-     lgtd_lifx_gateway_update_tag_refcounts(bulb->gw, bulb->state.tags, state->tags);
-@@ -140,6 +194,7 @@
-         LGTD_STATS_ADD_AND_UPDATE_PROCTITLE(
-             bulbs_powered_on, power == LGTD_LIFX_POWER_ON ? 1 : -1
-         );
-+        lgtd_router_send_to_device(bulb, LGTD_LIFX_GET_INFO, NULL);
-     }
- 
-     bulb->state.power = power;
-@@ -154,3 +209,53 @@
- 
-     bulb->state.tags = tags;
- }
-+
-+void
-+lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb,
-+                      enum lgtd_lifx_bulb_ips ip_id,
-+                      const struct lgtd_lifx_ip_state *state,
-+                      lgtd_time_mono_t received_at)
-+{
-+    assert(bulb);
-+    assert(state);
-+
-+    struct lgtd_lifx_bulb_ip *ip = &bulb->ips[ip_id];
-+    ip->state_updated_at = received_at;
-+    memcpy(&ip->state, state, sizeof(ip->state));
-+}
-+
-+void
-+lgtd_lifx_bulb_set_ip_firmware_info(struct lgtd_lifx_bulb *bulb,
-+                                    enum lgtd_lifx_bulb_ips ip_id,
-+                                    const struct lgtd_lifx_ip_firmware_info *info,
-+                                    lgtd_time_mono_t received_at)
-+{
-+    assert(bulb);
-+    assert(info);
-+
-+    struct lgtd_lifx_bulb_ip *ip = &bulb->ips[ip_id];
-+    ip->fw_info_updated_at = received_at;
-+    memcpy(&ip->fw_info, info, sizeof(ip->fw_info));
-+}
-+
-+void
-+lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *bulb,
-+                                const struct lgtd_lifx_product_info *info)
-+{
-+    assert(bulb);
-+    assert(info);
-+
-+    memcpy(&bulb->product_info, info, sizeof(bulb->product_info));
-+}
-+
-+void
-+lgtd_lifx_bulb_set_runtime_info(struct lgtd_lifx_bulb *bulb,
-+                                const struct lgtd_lifx_runtime_info *info,
-+                                lgtd_time_mono_t received_at)
-+{
-+    assert(bulb);
-+    assert(info);
-+
-+    bulb->runtime_info_updated_at = received_at;
-+    memcpy(&bulb->runtime_info, info, sizeof(bulb->runtime_info));
-+}
-diff --git a/lifx/bulb.h b/lifx/bulb.h
---- a/lifx/bulb.h
-+++ b/lifx/bulb.h
-@@ -30,17 +30,64 @@
-     char        label[LGTD_LIFX_LABEL_SIZE];
-     uint64_t    tags;
- };
-+
-+struct lgtd_lifx_ip_state {
-+    float       signal_strength;    // mW
-+    uint32_t    tx_bytes;
-+    uint32_t    rx_bytes;
-+    uint16_t    temperature;        // Deci-celcius: e.g 24.3 -> 2430
-+};
-+
-+struct lgtd_lifx_ip_firmware_info {
-+    uint64_t    built_at;       // ns since epoch
-+    uint64_t    installed_at;   // ns since epoch
-+    uint32_t    version;
-+};
-+
-+struct lgtd_lifx_product_info {
-+    uint32_t    vendor_id;
-+    uint32_t    product_id;
-+    uint32_t    version;
-+};
-+
-+struct lgtd_lifx_runtime_info {
-+    uint64_t    time;       // ns since epoch
-+    uint64_t    uptime;     // ns
-+    uint64_t    downtime;   // ns, last power off period duration
-+};
- #pragma pack(pop)
- 
-+enum { LGTD_LIFX_BULB_FETCH_HARDWARE_INFO_TIMER_MSECS = 5000 };
-+
-+enum lgtd_lifx_bulb_ips {
-+    LGTD_LIFX_BULB_MCU_IP = 0,
-+    LGTD_LIFX_BULB_WIFI_IP,
-+    LGTD_LIFX_BULB_IP_COUNT,
-+};
-+
-+// keyed with enum lgtd_lifx_bulb_ips:
-+extern const char * const lgtd_lifx_bulb_ip_names[];
-+
-+struct lgtd_lifx_bulb_ip {
-+    struct lgtd_lifx_ip_state           state;
-+    lgtd_time_mono_t                    state_updated_at;
-+    struct lgtd_lifx_ip_firmware_info   fw_info;
-+    lgtd_time_mono_t                    fw_info_updated_at;
-+};
-+
- struct lgtd_lifx_bulb {
-     RB_ENTRY(lgtd_lifx_bulb)        link;
-     SLIST_ENTRY(lgtd_lifx_bulb)     link_by_gw;
--    struct lgtd_lifx_gateway        *gw;
--    uint8_t                         addr[LGTD_LIFX_ADDR_LENGTH];
--    struct lgtd_lifx_light_state    state;
-     lgtd_time_mono_t                last_light_state_at;
-+    lgtd_time_mono_t                runtime_info_updated_at;
-     lgtd_time_mono_t                dirty_at;
-     uint16_t                        expected_power_on;
-+    uint8_t                         addr[LGTD_LIFX_ADDR_LENGTH];
-+    struct lgtd_lifx_gateway        *gw;
-+    struct lgtd_lifx_light_state    state;
-+    struct lgtd_lifx_bulb_ip        ips[LGTD_LIFX_BULB_IP_COUNT];
-+    struct lgtd_lifx_product_info   product_info;
-+    struct lgtd_lifx_runtime_info   runtime_info;
- };
- RB_HEAD(lgtd_lifx_bulb_map, lgtd_lifx_bulb);
- SLIST_HEAD(lgtd_lifx_bulb_list, lgtd_lifx_bulb);
-@@ -69,3 +116,17 @@
-                                     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);
-+
-+void lgtd_lifx_bulb_set_ip_state(struct lgtd_lifx_bulb *bulb,
-+                                        enum lgtd_lifx_bulb_ips ip_id,
-+                                        const struct lgtd_lifx_ip_state *state,
-+                                        lgtd_time_mono_t received_at);
-+void lgtd_lifx_bulb_set_ip_firmware_info(struct lgtd_lifx_bulb *bulb,
-+                                         enum lgtd_lifx_bulb_ips ip_id,
-+                                         const struct lgtd_lifx_ip_firmware_info *info,
-+                                         lgtd_time_mono_t received_at);
-+void lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *,
-+                                     const struct lgtd_lifx_product_info *);
-+void lgtd_lifx_bulb_set_runtime_info(struct lgtd_lifx_bulb *,
-+                                     const struct lgtd_lifx_runtime_info *,
-+                                     lgtd_time_mono_t);
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -15,12 +15,15 @@
- // 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/ioctl.h>
- #include <sys/queue.h>
-+#include <sys/socket.h>
- #include <sys/tree.h>
- #include <assert.h>
- #include <endian.h>
- #include <err.h>
- #include <errno.h>
-+#include <net/if_arp.h>
- #include <stdarg.h>
- #include <stdbool.h>
- #include <stdint.h>
-@@ -275,6 +278,28 @@
-     return bulb;
- }
- 
-+// TODO: make a separate shared library to resolve ip addresses to mac
-+// addresses:
-+static bool
-+lgtd_lifx_gateway_resolve_ipv4_addr(int socket,
-+                                    const struct sockaddr_in *sin,
-+                                    uint8_t *hw_addr)
-+{
-+    assert(sin);
-+    assert(hw_addr);
-+    assert(socket >= 0);
-+
-+    struct arpreq req;
-+    memset(&req, 0, sizeof(req));
-+    memcpy(&req.arp_pa, sin, sizeof(*sin));
-+    memcpy(req.arp_dev, "br0", 4);
-+    if (!ioctl(socket, SIOCGARP, &req)) {
-+        memcpy(hw_addr, req.arp_ha.sa_data, LGTD_LIFX_ADDR_LENGTH);
-+        return true;
-+    }
-+    return false;
-+}
-+
- struct lgtd_lifx_gateway *
- lgtd_lifx_gateway_open(const struct sockaddr_storage *peer,
-                        ev_socklen_t addrlen,
-@@ -310,10 +335,20 @@
-     gw->refresh_ev = evtimer_new(
-         lgtd_ev_base, lgtd_lifx_gateway_refresh_callback, gw
-     );
-+
-     memcpy(&gw->peer, peer, sizeof(gw->peer));
-     lgtd_sockaddrtoa(peer, gw->ip_addr, sizeof(gw->ip_addr));
-     gw->port = lgtd_sockaddrport(peer);
-     memcpy(gw->site.as_array, site, sizeof(gw->site.as_array));
-+    if (peer->ss_family == AF_INET) {
-+        bool ok = lgtd_lifx_gateway_resolve_ipv4_addr(
-+            gw->socket, (const struct sockaddr_in *)peer, gw->addr
-+        );
-+        if (!ok) {
-+            lgtd_warn("couldn't resolve %s to a LIFX address", gw->ip_addr);
-+        }
-+    }
-+
-     gw->last_req_at = received_at;
-     gw->next_req_at = received_at;
-     gw->last_pkt_at = received_at;
-@@ -489,12 +524,8 @@
-         (uintmax_t)pkt->tags
-     );
- 
--    struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
--        gw, hdr->target.device_addr
--    );
--    if (!b) {
--        return;
--    }
-+    struct lgtd_lifx_bulb *b;
-+    LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, hdr->target.device_addr);
- 
-     assert(sizeof(*pkt) == sizeof(b->state));
-     lgtd_lifx_bulb_set_light_state(
-@@ -519,7 +550,7 @@
-         }
-     }
- 
--    int latency = gw->last_pkt_at - gw->last_req_at;
-+    int latency = LGTD_LIFX_GATEWAY_LATENCY(gw);
-     if (latency < LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS) {
-         if (!event_pending(gw->refresh_ev, EV_TIMEOUT, NULL)) {
-             int timeout = LGTD_LIFX_GATEWAY_MIN_REFRESH_INTERVAL_MSECS - latency;
-@@ -559,14 +590,9 @@
-         gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr), pkt->power
-     );
- 
--    struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
--        gw, hdr->target.device_addr
-+    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-+        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_power_state, pkt->power
-     );
--    if (!b) {
--        return;
--    }
--
--    lgtd_lifx_bulb_set_power_state(b, pkt->power);
- }
- 
- int
-@@ -673,9 +699,10 @@
-     }
- }
- 
--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
-+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);
- 
-@@ -685,12 +712,8 @@
-         (uintmax_t)pkt->tags
-     );
- 
--    struct lgtd_lifx_bulb *b = lgtd_lifx_gateway_get_or_open_bulb(
--        gw, hdr->target.device_addr
--    );
--    if (!b) {
--        return;
--    }
-+    struct lgtd_lifx_bulb *b;
-+    LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, hdr->target.device_addr);
- 
-     int tag_id;
-     LGTD_LIFX_WIRE_FOREACH_TAG_ID(tag_id, pkt->tags) {
-@@ -707,3 +730,129 @@
- 
-     lgtd_lifx_bulb_set_tags(b, pkt->tags);
- }
-+
-+void
-+lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *gw,
-+                                  const struct lgtd_lifx_packet_header *hdr,
-+                                  const struct lgtd_lifx_packet_ip_state *pkt)
-+{
-+    assert(gw && hdr && pkt);
-+
-+    const char  *type;
-+    enum lgtd_lifx_bulb_ips ip_id;
-+    switch (hdr->packet_type) {
-+    case LGTD_LIFX_MESH_INFO:
-+        type = "MCU_STATE";
-+        ip_id = LGTD_LIFX_BULB_MCU_IP;
-+        break;
-+    case LGTD_LIFX_WIFI_INFO:
-+        type = "WIFI_STATE";
-+        ip_id = LGTD_LIFX_BULB_WIFI_IP;
-+        break;
-+    default:
-+        lgtd_info("invalid ip state packet_type %#hx", hdr->packet_type);
-+#ifndef NDEBUG
-+        abort();
-+#endif
-+        return;
-+    }
-+
-+    lgtd_debug(
-+        "%s <-- [%s]:%hu - %s "
-+        "signal_strength=%f, rx_bytes=%u, tx_bytes=%u, temperature=%hu",
-+        type, gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        pkt->signal_strength, pkt->rx_bytes, pkt->tx_bytes, pkt->temperature
-+    );
-+
-+    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-+        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_ip_state,
-+        ip_id, (const struct lgtd_lifx_ip_state *)pkt, gw->last_pkt_at
-+    );
-+}
-+
-+void
-+lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *gw,
-+                                          const struct lgtd_lifx_packet_header *hdr,
-+                                          const struct lgtd_lifx_packet_ip_firmware_info *pkt)
-+{
-+    assert(gw && hdr && pkt);
-+
-+    char built_at[64], installed_at[64];
-+    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->built_at, built_at);
-+    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->installed_at, installed_at);
-+
-+    const char  *type;
-+    enum lgtd_lifx_bulb_ips ip_id;
-+    switch (hdr->packet_type) {
-+    case LGTD_LIFX_MESH_FIRMWARE:
-+        type = "MCU_FIRMWARE_INFO";
-+        ip_id = LGTD_LIFX_BULB_MCU_IP;
-+        break;
-+    case LGTD_LIFX_WIFI_FIRMWARE_STATE:
-+        type = "WIFI_FIRMWARE_INFO";
-+        ip_id = LGTD_LIFX_BULB_WIFI_IP;
-+        break;
-+    default:
-+        lgtd_info("invalid ip firmware packet_type %#hx", hdr->packet_type);
-+#ifndef NDEBUG
-+        abort();
-+#endif
-+        return;
-+    }
-+
-+    lgtd_debug(
-+        "%s <-- [%s]:%hu - %s "
-+        "built_at=%s, installed_at=%s, version=%u",
-+        type, gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        built_at, installed_at, pkt->version
-+    );
-+
-+    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-+        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_ip_firmware_info,
-+        ip_id, (const struct lgtd_lifx_ip_firmware_info *)pkt, gw->last_pkt_at
-+    );
-+}
-+
-+void
-+lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *gw,
-+                                      const struct lgtd_lifx_packet_header *hdr,
-+                                      const struct lgtd_lifx_packet_product_info *pkt)
-+{
-+    assert(gw && hdr && pkt);
-+
-+    lgtd_debug(
-+        "PRODUCT_INFO <-- [%s]:%hu - %s "
-+        "vendor_id=%#x, product_id=%#x, version=%u",
-+        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        pkt->vendor_id, pkt->product_id, pkt->version
-+    );
-+
-+    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-+        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_product_info,
-+        (const struct lgtd_lifx_product_info *)pkt
-+    );
-+}
-+
-+void
-+lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *gw,
-+                                      const struct lgtd_lifx_packet_header *hdr,
-+                                      const struct lgtd_lifx_packet_runtime_info *pkt)
-+{
-+    assert(gw && hdr && pkt);
-+
-+    char device_time[64], uptime[64], downtime[64];
-+    LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(pkt->time, device_time);
-+    LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->uptime), uptime);
-+    LGTD_PRINT_DURATION(LGTD_NSECS_TO_SECS(pkt->downtime), downtime);
-+
-+    lgtd_debug(
-+        "PRODUCT_INFO <-- [%s]:%hu - %s time=%s, uptime=%s, downtime=%s",
-+        gw->ip_addr, gw->port, lgtd_addrtoa(hdr->target.device_addr),
-+        device_time, uptime, downtime
-+    );
-+
-+    LGTD_LIFX_GATEWAY_SET_BULB_ATTR(
-+        gw, hdr->target.device_addr, lgtd_lifx_bulb_set_runtime_info,
-+        (const struct lgtd_lifx_runtime_info *)pkt, gw->last_pkt_at
-+    );
-+}
-diff --git a/lifx/gateway.h b/lifx/gateway.h
---- a/lifx/gateway.h
-+++ b/lifx/gateway.h
-@@ -36,12 +36,19 @@
- struct lgtd_lifx_gateway {
-     LIST_ENTRY(lgtd_lifx_gateway)   link;
-     struct lgtd_lifx_bulb_list      bulbs;
-+#define LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, bulb_addr)  do {    \
-+    (b) = lgtd_lifx_gateway_get_or_open_bulb((gw), (bulb_addr));        \
-+    if (!(b)) {                                                         \
-+        return;                                                         \
-+    }                                                                   \
-+} while (0)
-     // Multiple gateways can share the same site (that happens when bulbs are
-     // far away enough that ZigBee can't be used). Moreover the SET_PAN_GATEWAY
-     // packet doesn't include the device address in the header (i.e: site and
-     // device_addr have the same value) so we have no choice but to use the
-     // remote ip address to identify a gateway:
-     struct sockaddr_storage         peer;
-+    uint8_t                         addr[LGTD_LIFX_ADDR_LENGTH];
-     char                            ip_addr[INET6_ADDRSTRLEN];
-     uint16_t                        port;
-     // TODO: just use an integer and rename it to site_id:
-@@ -60,6 +67,7 @@
-     lgtd_time_mono_t                last_req_at;
-     lgtd_time_mono_t                next_req_at;
-     lgtd_time_mono_t                last_pkt_at;
-+#define LGTD_LIFX_GATEWAY_LATENCY(gw) ((gw)->last_pkt_at - (gw)->last_req_at)
-     struct lgtd_lifx_message        pkt_ring[LGTD_LIFX_GATEWAY_PACKET_RING_SIZE];
- #define LGTD_LIFX_GATEWAY_INC_MESSAGE_RING_INDEX(idx)  do { \
-     (idx) += 1;                                             \
-@@ -77,6 +85,12 @@
- 
- extern struct lgtd_lifx_gateway_list lgtd_lifx_gateways;
- 
-+#define LGTD_LIFX_GATEWAY_SET_BULB_ATTR(gw, bulb_addr, bulb_fn, ...) do {   \
-+    struct lgtd_lifx_bulb *b;                                               \
-+    LGTD_LIFX_GATEWAY_GET_BULB_OR_RETURN(b, gw, bulb_addr);                 \
-+    (bulb_fn)(b, __VA_ARGS__);                                              \
-+} while (0)
-+
- struct lgtd_lifx_gateway *lgtd_lifx_gateway_get(const struct sockaddr_storage *);
- struct lgtd_lifx_gateway *lgtd_lifx_gateway_open(const struct sockaddr_storage *,
-                                                  ev_socklen_t,
-@@ -119,3 +133,15 @@
- void lgtd_lifx_gateway_handle_tags(struct lgtd_lifx_gateway *,
-                                    const struct lgtd_lifx_packet_header *,
-                                    const struct lgtd_lifx_packet_tags *);
-+void lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *,
-+                                        const struct lgtd_lifx_packet_header *,
-+                                        const struct lgtd_lifx_packet_ip_state *);
-+void lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *,
-+                                               const struct lgtd_lifx_packet_header *,
-+                                               const struct lgtd_lifx_packet_ip_firmware_info *);
-+void lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *,
-+                                           const struct lgtd_lifx_packet_header *,
-+                                           const struct lgtd_lifx_packet_product_info *);
-+void lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *,
-+                                           const struct lgtd_lifx_packet_header *,
-+                                           const struct lgtd_lifx_packet_runtime_info *);
-diff --git a/lifx/timer.c b/lifx/timer.c
---- a/lifx/timer.c
-+++ b/lifx/timer.c
-@@ -201,7 +201,6 @@
-         if (event_add(lgtd_lifx_timer_context.watchdog_interval_ev, &tv)) {
-             lgtd_err(1, "can't start watchdog");
-         }
--        lgtd_debug("starting watchdog timer");
-     }
- }
- 
-diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
---- a/lifx/wire_proto.c
-+++ b/lifx/wire_proto.c
-@@ -24,8 +24,10 @@
- #include <stdarg.h>
- #include <stdbool.h>
- #include <stdint.h>
-+#include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
-+#include <time.h>
- 
- #include <event2/util.h>
- 
-@@ -74,12 +76,32 @@
-     (void)pkt;
- }
- 
-+static void
-+lgtd_lifx_wire_enosys_packet_handler(struct lgtd_lifx_gateway *gw,
-+                                     const struct lgtd_lifx_packet_header *hdr,
-+                                     const void *pkt)
-+{
-+    (void)pkt;
-+
-+    const struct lgtd_lifx_packet_info *pkt_info;
-+    pkt_info = lgtd_lifx_wire_get_packet_info(hdr->packet_type);
-+    bool addressable = hdr->protocol & LGTD_LIFX_PROTOCOL_ADDRESSABLE;
-+    bool tagged = hdr->protocol & LGTD_LIFX_PROTOCOL_TAGGED;
-+    unsigned int protocol = hdr->protocol & LGTD_LIFX_PROTOCOL_VERSION_MASK;
-+    lgtd_info(
-+        "%s <-- [%s]:%hu - (Unimplemented, header info: "
-+        "addressable=%d, tagged=%d, protocol=%d)",
-+        pkt_info->name, gw->ip_addr, gw->port,
-+        addressable, tagged, protocol
-+    );
-+}
-+
- void
- lgtd_lifx_wire_load_packet_info_map(void)
- {
- #define DECODER(x)  ((void (*)(void *))(x))
- #define ENCODER(x)  ((void (*)(void *))(x))
--#define HANDLER(x)                                  \
-+#define HANDLER(x)                                      \
-     ((void (*)(struct lgtd_lifx_gateway *,              \
-                const struct lgtd_lifx_packet_header *,  \
-                const void *))(x))
-@@ -90,6 +112,10 @@
- #define REQUEST_ONLY                                        \
-     .decode = lgtd_lifx_wire_null_packet_encoder_decoder,   \
-     .handle = lgtd_lifx_wire_null_packet_handler
-+#define UNIMPLEMENTED                                       \
-+    .decode = lgtd_lifx_wire_null_packet_encoder_decoder,   \
-+    .encode = lgtd_lifx_wire_null_packet_encoder_decoder,   \
-+    .handle = lgtd_lifx_wire_enosys_packet_handler
- 
-     static struct lgtd_lifx_packet_info packet_table[] = {
-         // Gateway packets:
-@@ -187,6 +213,211 @@
-             .size = sizeof(struct lgtd_lifx_packet_tags),
-             .decode = DECODER(lgtd_lifx_wire_decode_tags),
-             .handle = HANDLER(lgtd_lifx_gateway_handle_tags)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            NO_PAYLOAD,
-+            .name = "GET_MESH_INFO",
-+            .type = LGTD_LIFX_GET_MESH_INFO
-+        },
-+        {
-+            RESPONSE_ONLY,
-+            .name = "MESH_INFO",
-+            .type = LGTD_LIFX_MESH_INFO,
-+            .size = sizeof(struct lgtd_lifx_packet_ip_state),
-+            .decode = DECODER(lgtd_lifx_wire_decode_ip_state),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_state)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            NO_PAYLOAD,
-+            .name = "GET_MESH_FIRMWARE",
-+            .type = LGTD_LIFX_GET_MESH_FIRMWARE
-+        },
-+        {
-+            RESPONSE_ONLY,
-+            .name = "MESH_FIRMWARE",
-+            .type = LGTD_LIFX_MESH_FIRMWARE,
-+            .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info),
-+            .decode = DECODER(lgtd_lifx_wire_decode_ip_firmware_info),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_firmware_info)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            NO_PAYLOAD,
-+            .name = "GET_WIFI_INFO",
-+            .type = LGTD_LIFX_GET_WIFI_INFO,
-+        },
-+        {
-+            RESPONSE_ONLY,
-+            .name = "WIFI_INFO",
-+            .type = LGTD_LIFX_WIFI_INFO,
-+            .size = sizeof(struct lgtd_lifx_packet_ip_state),
-+            .decode = DECODER(lgtd_lifx_wire_decode_ip_state),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_state)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            NO_PAYLOAD,
-+            .name = "GET_WIFI_FIRMWARE_STATE",
-+            .type = LGTD_LIFX_GET_WIFI_FIRMWARE_STATE
-+        },
-+        {
-+            RESPONSE_ONLY,
-+            .name = "WIFI_FIRMWARE_STATE",
-+            .type = LGTD_LIFX_WIFI_FIRMWARE_STATE,
-+            .size = sizeof(struct lgtd_lifx_packet_ip_firmware_info),
-+            .decode = DECODER(lgtd_lifx_wire_decode_ip_firmware_info),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_ip_firmware_info)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            NO_PAYLOAD,
-+            .name = "GET_VERSION",
-+            .type = LGTD_LIFX_GET_VERSION
-+        },
-+        {
-+            RESPONSE_ONLY,
-+            .name = "VERSION_STATE",
-+            .type = LGTD_LIFX_VERSION_STATE,
-+            .size = sizeof(struct lgtd_lifx_packet_product_info),
-+            .decode = DECODER(lgtd_lifx_wire_decode_product_info),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_product_info)
-+        },
-+        {
-+            REQUEST_ONLY,
-+            NO_PAYLOAD,
-+            .name = "GET_INFO",
-+            .type = LGTD_LIFX_GET_INFO
-+        },
-+        {
-+            RESPONSE_ONLY,
-+            .name = "INFO_STATE",
-+            .type = LGTD_LIFX_INFO_STATE,
-+            .size = sizeof(struct lgtd_lifx_packet_runtime_info),
-+            .decode = DECODER(lgtd_lifx_wire_decode_runtime_info),
-+            .handle = HANDLER(lgtd_lifx_gateway_handle_runtime_info)
-+        },
-+        // Unimplemented but "known" packets
-+        {
-+            UNIMPLEMENTED,
-+            .name = "GET_TIME",
-+            .type = LGTD_LIFX_GET_TIME
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "SET_TIME",
-+            .type = LGTD_LIFX_SET_TIME
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "TIME_STATE",
-+            .type = LGTD_LIFX_TIME_STATE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "GET_RESET_SWITCH_STATE",
-+            .type = LGTD_LIFX_GET_RESET_SWITCH_STATE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "RESET_SWITCH_STATE",
-+            .type = LGTD_LIFX_RESET_SWITCH_STATE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "GET_BULB_LABEL",
-+            .type = LGTD_LIFX_GET_BULB_LABEL
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "SET_BULB_LABEL",
-+            .type = LGTD_LIFX_SET_BULB_LABEL
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "BULB_LABEL",
-+            .type = LGTD_LIFX_BULB_LABEL
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "GET_MCU_RAIL_VOLTAGE",
-+            .type = LGTD_LIFX_GET_MCU_RAIL_VOLTAGE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "MCU_RAIL_VOLTAGE",
-+            .type = LGTD_LIFX_MCU_RAIL_VOLTAGE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "REBOOT",
-+            .type = LGTD_LIFX_REBOOT
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "SET_FACTORY_TEST_MODE",
-+            .type = LGTD_LIFX_SET_FACTORY_TEST_MODE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "DISABLE_FACTORY_TEST_MODE",
-+            .type = LGTD_LIFX_DISABLE_FACTORY_TEST_MODE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "ACK",
-+            .type = LGTD_LIFX_ACK
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "ECHO_REQUEST",
-+            .type = LGTD_LIFX_ECHO_REQUEST
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "ECHO_RESPONSE",
-+            .type = LGTD_LIFX_ECHO_RESPONSE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "SET_DIM_ABSOLUTE",
-+            .type = LGTD_LIFX_SET_DIM_ABSOLUTE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "SET_DIM_RELATIVE",
-+            .type = LGTD_LIFX_SET_DIM_RELATIVE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "GET_WIFI_STATE",
-+            .type = LGTD_LIFX_GET_WIFI_STATE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "SET_WIFI_STATE",
-+            .type = LGTD_LIFX_SET_WIFI_STATE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "WIFI_STATE",
-+            .type = LGTD_LIFX_WIFI_STATE
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "GET_ACCESS_POINTS",
-+            .type = LGTD_LIFX_GET_ACCESS_POINTS
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "SET_ACCESS_POINTS",
-+            .type = LGTD_LIFX_SET_ACCESS_POINTS
-+        },
-+        {
-+            UNIMPLEMENTED,
-+            .name = "ACCESS_POINT",
-+            .type = LGTD_LIFX_ACCESS_POINT
-         }
-     };
- 
-@@ -234,6 +465,24 @@
-     return LGTD_LIFX_WAVEFORM_INVALID;
- }
- 
-+void
-+lgtd_lifx_wire_print_nsec_timestamp(uint64_t nsec_ts, char *buf, int bufsz)
-+{
-+    assert(buf);
-+    assert(bufsz > 0);
-+
-+    time_t ts = LGTD_NSECS_TO_SECS(nsec_ts);
-+
-+    struct tm tm_utc;
-+    if (gmtime_r(&ts, &tm_utc)) {
-+        int64_t usecs = LGTD_NSECS_TO_USECS(nsec_ts - LGTD_SECS_TO_NSECS(ts));
-+        LGTD_TM_TO_ISOTIME(&tm_utc, buf, bufsz, usecs);
-+        return;
-+    }
-+
-+    buf[0] = '\0';
-+}
-+
- static void
- lgtd_lifx_wire_encode_header(struct lgtd_lifx_packet_header *hdr, int flags)
- {
-@@ -423,3 +672,44 @@
- 
-     pkt->tags = le64toh(pkt->tags);
- }
-+
-+void
-+lgtd_lifx_wire_decode_ip_state(struct lgtd_lifx_packet_ip_state *pkt)
-+{
-+    assert(pkt);
-+
-+    pkt->signal_strength = lgtd_lifx_wire_lefloattoh(pkt->signal_strength);
-+    pkt->tx_bytes = le32toh(pkt->tx_bytes);
-+    pkt->rx_bytes = le32toh(pkt->rx_bytes);
-+    pkt->temperature = le16toh(pkt->temperature);
-+}
-+
-+void
-+lgtd_lifx_wire_decode_ip_firmware_info(struct lgtd_lifx_packet_ip_firmware_info *pkt)
-+{
-+    assert(pkt);
-+
-+    pkt->built_at = le64toh(pkt->built_at);
-+    pkt->installed_at = le64toh(pkt->installed_at);
-+    pkt->version = le32toh(pkt->version);
-+}
-+
-+void
-+lgtd_lifx_wire_decode_product_info(struct lgtd_lifx_packet_product_info *pkt)
-+{
-+    assert(pkt);
-+
-+    pkt->vendor_id = le32toh(pkt->vendor_id);
-+    pkt->product_id = le32toh(pkt->product_id);
-+    pkt->version = le32toh(pkt->version);
-+}
-+
-+void
-+lgtd_lifx_wire_decode_runtime_info(struct lgtd_lifx_packet_runtime_info *pkt)
-+{
-+    assert(pkt);
-+
-+    pkt->time = le64toh(pkt->time);
-+    pkt->uptime = le64toh(pkt->uptime);
-+    pkt->downtime = le64toh(pkt->downtime);
-+}
-diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
---- a/lifx/wire_proto.h
-+++ b/lifx/wire_proto.h
-@@ -28,7 +28,7 @@
- static inline floatle_t
- lgtd_lifx_wire_htolefloat(float f)
- {
--    union { float f; uint32_t i; } u  = { .f = f };
-+    union { float f; uint32_t i; } u = { .f = f };
-     htole32(u.i);
-     return u.f;
- }
-@@ -36,7 +36,7 @@
- static inline floatle_t
- lgtd_lifx_wire_lefloattoh(float f)
- {
--    union { float f; uint32_t i; } u  = { .f = f };
-+    union { float f; uint32_t i; } u = { .f = f };
-     le32toh(u.i);
-     return u.f;
- }
-@@ -249,6 +249,30 @@
-     char        label[LGTD_LIFX_LABEL_SIZE];
- };
- 
-+struct lgtd_lifx_packet_ip_state {
-+    floatle_t   signal_strength;
-+    uint32le_t  tx_bytes;
-+    uint32le_t  rx_bytes;
-+    uint16le_t  temperature;
-+};
-+
-+struct lgtd_lifx_packet_ip_firmware_info {
-+    uint64le_t  built_at;
-+    uint64le_t  installed_at;
-+    uint32le_t  version;
-+};
-+
-+struct lgtd_lifx_packet_product_info {
-+    uint32le_t  vendor_id;
-+    uint32le_t  product_id;
-+    uint32le_t  version;
-+};
-+
-+struct lgtd_lifx_packet_runtime_info {
-+    uint64le_t  time;
-+    uint64le_t  uptime;
-+    uint64le_t  downtime;
-+};
- #pragma pack(pop)
- 
- enum lgtd_lifx_header_flags {
-@@ -330,8 +354,11 @@
-          (tag_id_varname) != -1;                                                    \
-          (tag_id_varname) = lgtd_lifx_wire_next_tag_id((tag_id_varname), (tags)))
- 
--
- enum lgtd_lifx_waveform_type lgtd_lifx_wire_waveform_string_id_to_type(const char *, int);
-+void lgtd_lifx_wire_print_nsec_timestamp(uint64_t, char *, int);
-+#define LGTD_LIFX_WIRE_PRINT_NSEC_TIMESTAMP(ts, arr) do {               \
-+    lgtd_lifx_wire_print_nsec_timestamp((ts), (arr), sizeof((arr)));    \
-+} while (0)
- 
- const struct lgtd_lifx_packet_info *lgtd_lifx_wire_get_packet_info(enum lgtd_lifx_packet_type);
- void lgtd_lifx_wire_load_packet_info_map(void);
-@@ -356,3 +383,8 @@
- 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 *);
-+
-+void lgtd_lifx_wire_decode_ip_state(struct lgtd_lifx_packet_ip_state *);
-+void lgtd_lifx_wire_decode_ip_firmware_info(struct lgtd_lifx_packet_ip_firmware_info *);
-+void lgtd_lifx_wire_decode_product_info(struct lgtd_lifx_packet_product_info *);
-+void lgtd_lifx_wire_decode_runtime_info(struct lgtd_lifx_packet_runtime_info *);
-diff --git a/tests/lifx/mock_gateway.h b/tests/lifx/mock_gateway.h
---- a/tests/lifx/mock_gateway.h
-+++ b/tests/lifx/mock_gateway.h
-@@ -129,3 +129,51 @@
-     (void)pkt_tags;
- }
- #endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_IP_STATE
-+void
-+lgtd_lifx_gateway_handle_ip_state(struct lgtd_lifx_gateway *gw,
-+                                  const struct lgtd_lifx_packet_header *hdr,
-+                                  const struct lgtd_lifx_packet_ip_state *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_IP_FIRMWARE_INFO
-+void
-+lgtd_lifx_gateway_handle_ip_firmware_info(struct lgtd_lifx_gateway *gw,
-+                                          const struct lgtd_lifx_packet_header *hdr,
-+                                          const struct lgtd_lifx_packet_ip_firmware_info *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_PRODUCT_INFO
-+void
-+lgtd_lifx_gateway_handle_product_info(struct lgtd_lifx_gateway *gw,
-+                                      const struct lgtd_lifx_packet_header *hdr,
-+                                      const struct lgtd_lifx_packet_product_info *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_GATEWAY_HANDLE_RUNTIME_INFO
-+void
-+lgtd_lifx_gateway_handle_runtime_info(struct lgtd_lifx_gateway *gw,
-+                                      const struct lgtd_lifx_packet_header *hdr,
-+                                      const struct lgtd_lifx_packet_runtime_info *pkt)
-+{
-+    (void)gw;
-+    (void)hdr;
-+    (void)pkt;
-+}
-+#endif
-diff --git a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
---- a/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
-+++ b/tests/lifx/wire_proto/test_wire_proto_encode_decode_header.c
-@@ -2,7 +2,7 @@
- 
- #include "wire_proto.c"
- 
--#include "test_wire_proto_utils.h"
-+#include "mock_gateway.h"
- 
- int
- main(void)
-diff --git a/tests/lifx/wire_proto/test_wire_proto_utils.h b/tests/lifx/wire_proto/test_wire_proto_utils.h
-deleted file mode 100644
---- a/tests/lifx/wire_proto/test_wire_proto_utils.h
-+++ /dev/null
-@@ -1,46 +0,0 @@
--#pragma once
--
--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;
--}
--
--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/lifx/wire_proto/test_wire_proto_waveform_table.c b/tests/lifx/wire_proto/test_wire_proto_waveform_table.c
---- a/tests/lifx/wire_proto/test_wire_proto_waveform_table.c
-+++ b/tests/lifx/wire_proto/test_wire_proto_waveform_table.c
-@@ -1,6 +1,6 @@
- #include "wire_proto.c"
- 
--#include "test_wire_proto_utils.h"
-+#include "mock_gateway.h"
- 
- int
- main(void)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/make_device_timeout_2_5x_force_refresh.patch	Sun Aug 16 00:48:06 2015 -0700
@@ -0,0 +1,18 @@
+# HG changeset patch
+# Parent  81c70d56434dba896d023f0f182d81de64b00a4e
+Bump the device timeout to 2.5x device force refresh
+
+This will let us do more retries before we drop devices.
+
+diff --git a/lifx/timer.h b/lifx/timer.h
+--- a/lifx/timer.h
++++ b/lifx/timer.h
+@@ -20,7 +20,7 @@
+ 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 = 3000 };
++enum { LGTD_LIFX_TIMER_DEVICE_TIMEOUT_MSECS = 5000 };
+ enum { LGTD_LIFX_TIMER_DEVICE_FORCE_REFRESH_MSECS = 2000 };
+ 
+ bool lgtd_lifx_timer_setup(void);
--- a/series	Sat Aug 15 23:22:19 2015 -0700
+++ b/series	Sun Aug 16 00:48:06 2015 -0700
@@ -1,4 +1,5 @@
+fix_addrtoa_mess.patch
+make_device_timeout_2_5x_force_refresh.patch
 add_start_stop_timer.patch
-implement_some_metadata_packet_types.patch
-fix_sockaddrtoa_mess.patch
-blublu
+expose_a_bunch_of_metadata_in_get_light_state.patch
+add_ipv4_to_ieee8023mac.patch #+future #+linux