view expose_a_bunch_of_metadata_in_get_light_state.patch @ 253:aecc19ba45d9

patch reorg
author Louis Opter <kalessin@kalessin.fr>
date Sun, 16 Aug 2015 00:48:06 -0700
parents
children f0ae1e79c4b0
line wrap: on
line source

# 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)