view add_power_transition.patch @ 458:89ae4a498401

Add a bit more content to that effect test
author Louis Opter <kalessin@kalessin.fr>
date Mon, 23 May 2016 01:39:18 -0400
parents 5868dc7734b0
children cd963f484771
line wrap: on
line source

# HG changeset patch
# Parent  f2d3c102353fd3d3ca1ab2a8a27d7961458a25a6
Add a transition argument to the power functions

Unlike LIFX's implementation, lightsd will properly get the bulbs to
update their brightness state during the transition. That is: if you
call get_light_state during the transition you will see the brightness
of the bulb being updated.

This is achieved by combining SET_LIGHT_COLOR and SET_POWER_STATE
commands in a compatible way with the official LIFX mobile apps.

This feature doesn't work for Original bulbs still running firmware 1.1
since you can't set their color while they are off. The oldest firmware
version tested for the Original bulbs is 1.5.

diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -99,6 +99,7 @@
 
 ADD_SUBDIRECTORY(compat)
 ADD_SUBDIRECTORY(core)
+ADD_SUBDIRECTORY(effects)
 ADD_SUBDIRECTORY(lifx)
 
 # 2.8.11 is the first version with TARGET_INCLUDE_DIRECTORIES:
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -3,8 +3,7 @@
 
 INCLUDE_DIRECTORIES(
     ${CMAKE_CURRENT_SOURCE_DIR}/../
-    ${CMAKE_CURRENT_SOURCE_DIR}
-    ${CMAKE_CURRENT_BINARY_DIR}/../
+    ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/../
     ${CMAKE_CURRENT_BINARY_DIR}
 )
 
@@ -15,6 +14,7 @@
     client.c
     console.c
     daemon.c
+    effect.c
     jsmn.c
     jsonrpc.c
     listen.c
@@ -30,7 +30,7 @@
 )
 
 TARGET_LINK_LIBRARIES(
-    lightsd lifx ${EVENT2_CORE_LIBRARY} ${TIME_MONOTONIC_LIBRARY}
+    lightsd effects lifx ${EVENT2_CORE_LIBRARY} ${TIME_MONOTONIC_LIBRARY}
 )
 
 INSTALL(TARGETS lightsd RUNTIME DESTINATION bin)
diff --git a/core/effect.c b/core/effect.c
new file mode 100644
--- /dev/null
+++ b/core/effect.c
@@ -0,0 +1,171 @@
+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
+//
+// This file is part of lighstd.
+//
+// lighstd is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// lighstd is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
+
+#include <sys/queue.h>
+#include <sys/tree.h>
+#include <assert.h>
+#include <endian.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include <event2/event.h>
+
+#include "time_monotonic.h"
+#include "timer.h"
+#include "lifx/wire_proto.h"
+#include "proto.h"
+#include "effect.h"
+#include "lightsd.h"
+
+struct lgtd_effect_list lgtd_effects = LIST_HEAD_INITIALIZER(&lgtd_effects);
+
+void
+lgtd_effect_stop(struct lgtd_effect *effect)
+{
+    assert(effect);
+
+    lgtd_info("ending effect %s, id=%p", effect->name, effect);
+    LIST_REMOVE(effect, link);
+    if (effect->timer) {
+        lgtd_timer_stop(effect->timer);
+    }
+    free(effect);
+}
+
+static void
+lgtd_effect_duration_callback(struct lgtd_timer *timer, union lgtd_timer_ctx ctx)
+{
+    struct lgtd_effect *effect = (struct lgtd_effect *)ctx.as_ptr;
+    if (effect->duration_cb) {
+        effect->duration_cb(effect);
+    }
+    lgtd_timer_stop(timer);
+    lgtd_effect_stop(effect);
+}
+
+static void
+lgtd_effect_timer_callback(struct lgtd_timer *timer, union lgtd_timer_ctx ctx)
+{
+    (void)timer;
+
+    struct lgtd_effect *effect = (struct lgtd_effect *)ctx.as_ptr;
+
+    lgtd_time_mono_t now = lgtd_time_monotonic_msecs();
+    if (now < effect->created_at + effect->duration) { // avoid overflow
+        lgtd_time_mono_t diff = now - effect->created_at + effect->duration;
+        // maybe the computer was sleepy
+        if (diff > LGTD_EFFECT_STALE_THRESHOLD_MSECS) {
+            lgtd_warnx(
+                "stopping stale effect %s created %jums ago, id=%p, "
+                "duration=%dms",
+                effect->name, (uintmax_t)diff, effect, effect->duration
+            );
+            lgtd_effect_stop(effect);
+            return;
+        }
+    }
+
+    effect->apply_cb(effect);
+    effect->apply_cnt++;
+}
+
+struct lgtd_effect *
+lgtd_effect_start(const char *name,
+                  int duration,
+                  void (*duration_cb)(const struct lgtd_effect *),
+                  int timer_flags,
+                  int timer_ms,
+                  void (*apply_cb)(const struct lgtd_effect *),
+                  union lgtd_effect_ctx ctx)
+{
+    assert(name);
+    assert(timer_ms >= 0);
+    assert(timer_ms < duration);
+    if (timer_ms) {
+        assert(apply_cb);
+    }
+
+    struct lgtd_effect *effect = calloc(1, sizeof(*effect));
+    if (!effect) {
+        return NULL;
+    }
+
+    effect->name = name;
+    effect->created_at = lgtd_time_monotonic_msecs();
+    effect->duration = duration;
+    effect->duration_cb = duration_cb;
+    effect->apply_cb = apply_cb;
+    effect->ctx = ctx;
+    LIST_INSERT_HEAD(&lgtd_effects, effect, link);
+
+    union lgtd_timer_ctx timer_ctx = { .as_ptr = effect };
+
+    struct lgtd_timer *duration_timer = NULL;
+    if (duration) {
+        duration_timer = lgtd_timer_start(
+            LGTD_TIMER_DEFAULT_FLAGS,
+            duration,
+            lgtd_effect_duration_callback,
+            timer_ctx
+        );
+        if (!duration_timer) {
+            goto err;
+        }
+    }
+
+    if (timer_ms) {
+        effect->timer = lgtd_timer_start(
+            timer_flags, timer_ms, lgtd_effect_timer_callback, timer_ctx
+        );
+        if (!effect->timer) {
+            goto err;
+        }
+        lgtd_info(
+            "starting effect %s, id=%p, duration=%dms, tick=%dms",
+            name, effect, duration, timer_ms
+        );
+    } else {
+        lgtd_info(
+            "starting effect %s, id=%p, duration=%dms", name, effect, duration
+        );
+        if (effect->apply_cb) {
+            effect->apply_cb(effect);
+        }
+    }
+
+    return effect;
+
+err:
+    if (duration_timer) {
+        lgtd_timer_stop(duration_timer);
+    }
+    if (effect->timer) {
+        lgtd_timer_stop(effect->timer);
+    }
+    free(effect);
+    return NULL;
+}
+
+void
+lgtd_effect_stop_all(void)
+{
+    while (!LIST_EMPTY(&lgtd_effects)) {
+        lgtd_effect_stop(LIST_FIRST(&lgtd_effects));
+    }
+}
diff --git a/core/effect.h b/core/effect.h
new file mode 100644
--- /dev/null
+++ b/core/effect.h
@@ -0,0 +1,56 @@
+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
+//
+// This file is part of lighstd.
+//
+// lighstd is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// lighstd is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
+
+#pragma once
+
+enum { LGTD_EFFECT_STALE_THRESHOLD_MSECS = 60 * 1000 };
+
+union lgtd_effect_ctx {
+    uint64_t    as_uint;
+    void        *as_ptr;
+};
+
+struct lgtd_effect {
+    LIST_ENTRY(lgtd_effect)         link;
+    const char                      *name;
+    lgtd_time_mono_t                created_at;
+    int                             duration;
+    void                            (*duration_cb)(const struct lgtd_effect *);
+    struct lgtd_timer               *timer;
+    void                            (*apply_cb)(const struct lgtd_effect *);
+    uint32_t                        apply_cnt;
+    union lgtd_effect_ctx           ctx;
+};
+LIST_HEAD(lgtd_effect_list, lgtd_effect);
+
+extern struct lgtd_effect_list lgtd_effects;
+
+static inline uintptr_t
+lgtd_effect_id(const struct lgtd_effect *effect)
+{
+    return (uintptr_t)effect;
+}
+
+struct lgtd_effect *lgtd_effect_start(const char *,
+                                      int, // duration
+                                      void (*)(const struct lgtd_effect *), // duration cb
+                                      int, // timer flags
+                                      int, // timer ms
+                                      void (*)(const struct lgtd_effect *), // apply cb
+                                      union lgtd_effect_ctx);
+void lgtd_effect_stop(struct lgtd_effect *);
+void lgtd_effect_stop_all(void);
diff --git a/core/jsonrpc.c b/core/jsonrpc.c
--- a/core/jsonrpc.c
+++ b/core/jsonrpc.c
@@ -119,17 +119,6 @@
 }
 
 static bool
-lgtd_jsonrpc_type_number(const jsmntok_t *t, const char *json)
-{
-    if (t->type != JSMN_PRIMITIVE) {
-        return false;
-    }
-
-    char c = json[t->start];
-    return c == '-' || (c >= '0' && c <= '9');
-}
-
-static bool
 lgtd_jsonrpc_type_bool(const jsmntok_t *t, const char *json)
 {
     if (t->type != JSMN_PRIMITIVE) {
@@ -177,20 +166,20 @@
 }
 
 static bool
-lgtd_jsonrpc_type_string_number_or_null(const jsmntok_t *t,
-                                        const char *json)
+lgtd_jsonrpc_type_string_integer_or_null(const jsmntok_t *t,
+                                         const char *json)
 {
-    return lgtd_jsonrpc_type_number(t, json)
+    return lgtd_jsonrpc_type_integer(t, json)
         || lgtd_jsonrpc_type_null(t, json)
         || lgtd_jsonrpc_type_string(t, json);
 }
 
 static bool
-lgtd_jsonrpc_type_string_or_number(const jsmntok_t *t,
-                                   const char *json)
+lgtd_jsonrpc_type_string_or_integer(const jsmntok_t *t,
+                                    const char *json)
 {
     return lgtd_jsonrpc_type_string(t, json)
-        || lgtd_jsonrpc_type_number(t, json);
+        || lgtd_jsonrpc_type_integer(t, json);
 }
 
 static bool __attribute__((unused))
@@ -201,10 +190,10 @@
 }
 
 static bool
-lgtd_jsonrpc_type_string_number_or_array(const jsmntok_t *t, const char *json)
+lgtd_jsonrpc_type_string_integer_or_array(const jsmntok_t *t, const char *json)
 {
     return lgtd_jsonrpc_type_string(t, json)
-        || lgtd_jsonrpc_type_number(t, json)
+        || lgtd_jsonrpc_type_integer(t, json)
         || lgtd_jsonrpc_type_array(t, json);
 }
 
@@ -591,7 +580,7 @@
             "id",
             offsetof(struct lgtd_jsonrpc_request, id),
             -1,
-            lgtd_jsonrpc_type_string_number_or_null,
+            lgtd_jsonrpc_type_string_integer_or_null,
             true
         )
     };
@@ -642,7 +631,7 @@
 
     for (int ti = target_ntokens; ti--;) {
         int token_len = LGTD_JSONRPC_TOKEN_LEN(&target[ti]);
-        if (lgtd_jsonrpc_type_string_or_number(&target[ti], client->json)) {
+        if (lgtd_jsonrpc_type_string_or_integer(&target[ti], client->json)) {
             struct lgtd_proto_target *t = malloc(sizeof(*t) + token_len + 1);
             if (!t) {
                 lgtd_warn("can't allocate a new target");
@@ -691,7 +680,7 @@
             "target",
             offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, target),
             offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, target_ntokens),
-            lgtd_jsonrpc_type_string_number_or_array,
+            lgtd_jsonrpc_type_string_integer_or_array,
             false
         ),
         LGTD_JSONRPC_NODE(
@@ -804,7 +793,7 @@
             "target",
             offsetof(struct lgtd_jsonrpc_set_waveform_args, target),
             offsetof(struct lgtd_jsonrpc_set_waveform_args, target_ntokens),
-            lgtd_jsonrpc_type_string_number_or_array,
+            lgtd_jsonrpc_type_string_integer_or_array,
             false
         ),
         LGTD_JSONRPC_NODE(
@@ -946,6 +935,89 @@
 }
 
 static bool
+lgtd_jsonrpc_extract_target_list_and_transition(struct lgtd_proto_target_list *targets,
+                                                int *transition,
+                                                struct lgtd_client *client)
+{
+    struct lgtd_jsonrpc_power_args {
+        const jsmntok_t *target;
+        int             target_ntokens;
+        const jsmntok_t *transition;
+    } params = { NULL, 0, NULL };
+    static const struct lgtd_jsonrpc_node schema[] = {
+        LGTD_JSONRPC_NODE(
+            "target",
+            offsetof(struct lgtd_jsonrpc_power_args, target),
+            offsetof(struct lgtd_jsonrpc_power_args, target_ntokens),
+            lgtd_jsonrpc_type_string_integer_or_array,
+            false
+        ),
+        LGTD_JSONRPC_NODE(
+            "transition",
+            offsetof(struct lgtd_jsonrpc_power_args, transition),
+            -1,
+            lgtd_jsonrpc_type_integer,
+            true
+        )
+    };
+
+    assert(transition);
+
+    struct lgtd_jsonrpc_request *req = client->current_request;
+    bool ok = lgtd_jsonrpc_extract_and_validate_params_against_schema(
+        &params,
+        schema,
+        LGTD_ARRAY_SIZE(schema),
+        req->params,
+        req->params_ntokens,
+        client->json
+    );
+    if (!ok) {
+        goto error_invalid_params;
+    }
+
+    if (params.transition) {
+        *transition = strtol(&client->json[params.transition->start], NULL, 10);
+        if (*transition < 0 || errno == ERANGE) {
+            goto error_invalid_params;
+        }
+    } else {
+        *transition = 0;
+    }
+
+    return lgtd_jsonrpc_build_target_list(
+        targets, client, params.target, params.target_ntokens
+    );
+
+error_invalid_params:
+    lgtd_jsonrpc_send_error(
+        client, LGTD_JSONRPC_INVALID_PARAMS, "Invalid parameters"
+    );
+    return false;
+}
+
+#define CHECK_AND_CALL_POWER_METHOD(proto_method)                               \
+static void                                                                     \
+lgtd_jsonrpc_check_and_call_##proto_method(struct lgtd_client *client)          \
+{                                                                               \
+    struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets);   \
+    int transition = 0;                                                         \
+    bool ok = lgtd_jsonrpc_extract_target_list_and_transition(                  \
+        &targets, &transition, client                                           \
+    );                                                                          \
+    if (!ok) {                                                                  \
+        return;                                                                 \
+    }                                                                           \
+                                                                                \
+    lgtd_proto_##proto_method(client, &targets, transition);                    \
+    lgtd_proto_target_list_clear(&targets);                                     \
+}
+
+CHECK_AND_CALL_POWER_METHOD(power_on);
+CHECK_AND_CALL_POWER_METHOD(power_off);
+CHECK_AND_CALL_POWER_METHOD(power_toggle);
+
+static bool
 lgtd_jsonrpc_extract_target_list(struct lgtd_proto_target_list *targets,
                                  struct lgtd_client *client)
 {
@@ -958,7 +1030,7 @@
             "target",
             offsetof(struct lgtd_jsonrpc_target_args, target),
             offsetof(struct lgtd_jsonrpc_target_args, target_ntokens),
-            lgtd_jsonrpc_type_string_number_or_array,
+            lgtd_jsonrpc_type_string_integer_or_array,
             false
         )
     };
@@ -979,25 +1051,19 @@
     );
 }
 
-#define CHECK_AND_CALL_TARGETS_ONLY_METHOD(proto_method)                       \
-static void                                                                    \
-lgtd_jsonrpc_check_and_call_##proto_method(struct lgtd_client *client)         \
-{                                                                              \
-    struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets);  \
-    bool ok = lgtd_jsonrpc_extract_target_list(&targets, client);              \
-    if (!ok) {                                                                 \
-        return;                                                                \
-    }                                                                          \
-                                                                               \
-    lgtd_proto_##proto_method(client, &targets);                               \
-    lgtd_proto_target_list_clear(&targets);                                    \
+static void
+lgtd_jsonrpc_check_and_call_get_light_state(struct lgtd_client *client)
+{
+    struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets);
+    bool ok = lgtd_jsonrpc_extract_target_list(&targets, client);
+    if (!ok) {
+        return;
+    }
+
+    lgtd_proto_get_light_state(client, &targets);
+    lgtd_proto_target_list_clear(&targets);
 }
 
-CHECK_AND_CALL_TARGETS_ONLY_METHOD(power_on);
-CHECK_AND_CALL_TARGETS_ONLY_METHOD(power_off);
-CHECK_AND_CALL_TARGETS_ONLY_METHOD(power_toggle);
-CHECK_AND_CALL_TARGETS_ONLY_METHOD(get_light_state);
-
 static void
 lgtd_jsonrpc_check_and_call_proto_tag_or_untag_or_set_label(
         struct lgtd_client *client,
@@ -1016,7 +1082,7 @@
             "target",
             offsetof(struct lgtd_jsonrpc_target_args, target),
             offsetof(struct lgtd_jsonrpc_target_args, target_ntokens),
-            lgtd_jsonrpc_type_string_number_or_array,
+            lgtd_jsonrpc_type_string_integer_or_array,
             false
         ),
         LGTD_JSONRPC_NODE(
diff --git a/core/lightsd.c b/core/lightsd.c
--- a/core/lightsd.c
+++ b/core/lightsd.c
@@ -51,6 +51,7 @@
 #include "timer.h"
 #include "listen.h"
 #include "daemon.h"
+#include "effect.h"
 #include "lightsd.h"
 
 struct lgtd_opts lgtd_opts = {
@@ -188,6 +189,7 @@
     lgtd_listen_close_all();
     lgtd_command_pipe_close_all();
     lgtd_client_close_all();
+    lgtd_effect_stop_all();
     lgtd_lifx_broadcast_close();
     lgtd_lifx_gateway_close_all();
     lgtd_timer_stop_all();
diff --git a/core/lightsd.h b/core/lightsd.h
--- a/core/lightsd.h
+++ b/core/lightsd.h
@@ -137,9 +137,9 @@
 extern struct event_base *lgtd_ev_base;
 extern const char *lgtd_progname;
 
-char *lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen);
+char *lgtd_ieee8023mactoa(const uint8_t *addr, char *buf, int buflen);
 #define LGTD_IEEE8023MACTOA(addr, buf) \
-    lgtd_iee8023mactoa((addr), (buf), sizeof(buf))
+    lgtd_ieee8023mactoa((addr), (buf), sizeof(buf))
 char *lgtd_sockaddrtoa(const struct sockaddr *, char *buf, int buflen);
 #define LGTD_SOCKADDRTOA(addr, buf) \
     lgtd_sockaddrtoa((addr), (buf), sizeof(buf))
diff --git a/core/proto.c b/core/proto.c
--- a/core/proto.c
+++ b/core/proto.c
@@ -39,6 +39,7 @@
 #include "client.h"
 #include "lifx/gateway.h"
 #include "proto.h"
+#include "effects/power_transition.h"
 #include "router.h"
 #include "lightsd.h"
 
@@ -58,23 +59,68 @@
     }
 }
 
+int
+lgtd_proto_target_list_len(const struct lgtd_proto_target_list *targets)
+{
+    // TODO: carry the len of the list in the struct and set it from
+    //       lgtd_jsonrpc_build_target_list.
+    int len = 0;
+    struct lgtd_proto_target *it;
+    SLIST_FOREACH(it, targets, link) {
+        len++;
+    }
+    return len;
+}
+
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     assert(targets);
+    assert(transition >= 0);
 
-    struct lgtd_lifx_packet_power_state pkt = { .power = LGTD_LIFX_POWER_ON };
-    SEND_RESULT(
-        client, lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &pkt)
-    );
+    bool ok;
+    if (transition) {
+        ok = lgtd_effect_power_transition(
+            targets, LGTD_EFFECT_POWER_TRANSITION_ON, transition
+        );
+    } else {
+        struct lgtd_lifx_packet_power pkt = { .power = LGTD_LIFX_POWER_ON };
+        ok = lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &pkt);
+    }
+
+    SEND_RESULT(client, ok);
+}
+
+void
+lgtd_proto_power_off(struct lgtd_client *client,
+                     const struct lgtd_proto_target_list *targets,
+                     int transition)
+{
+    assert(targets);
+    assert(transition >= 0);
+
+    bool ok;
+    if (transition) {
+        ok = lgtd_effect_power_transition(
+            targets, LGTD_EFFECT_POWER_TRANSITION_OFF, transition
+        );
+    } else {
+        struct lgtd_lifx_packet_power pkt = { .power = LGTD_LIFX_POWER_OFF };
+        ok = lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &pkt);
+    }
+
+    SEND_RESULT(client, ok);
 }
 
 void
 lgtd_proto_power_toggle(struct lgtd_client *client,
-                        const struct lgtd_proto_target_list *targets)
+                        const struct lgtd_proto_target_list *targets,
+                        int transition)
 {
     assert(targets);
+    assert(transition >= 0);
 
     struct lgtd_router_device_list *devices = NULL;
     devices = lgtd_router_targets_to_devices(targets);
@@ -85,33 +131,67 @@
         return;
     }
 
+    struct lgtd_proto_target_list targets_to_turn_off =
+        SLIST_HEAD_INITIALIZER(&targets_to_turn_off);
+    struct lgtd_proto_target_list targets_to_turn_on =
+        SLIST_HEAD_INITIALIZER(&targets_to_turn_on);
+
+    bool ok = true;
     struct lgtd_router_device *device;
     SLIST_FOREACH(device, devices, link) {
         struct lgtd_lifx_bulb *bulb = device->device;
-        struct lgtd_lifx_packet_power_state pkt = {
-            .power = ~bulb->state.power
-        };
-        lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_POWER_STATE, &pkt);
+        if (transition) {
+            struct lgtd_proto_target *target = malloc(
+                offsetof(struct lgtd_proto_target, target)
+                + LGTD_LIFX_ADDR_STRLEN
+            );
+            if (!target) {
+                lgtd_warn("couldn't toggle bulbs");
+                ok = false;
+                break;
+            }
+            snprintf(
+                target->target, LGTD_LIFX_ADDR_STRLEN,
+                "%02x%02x%02x%02x%02x%02x",
+                bulb->addr[0], bulb->addr[1], bulb->addr[2], bulb->addr[3],
+                bulb->addr[4], bulb->addr[5]
+            );
+            SLIST_INSERT_HEAD(
+                bulb->state.power ? &targets_to_turn_off : &targets_to_turn_on,
+                target,
+                link
+            );
+        } else {
+            struct lgtd_lifx_packet_power_state pkt = {
+                .power = ~bulb->state.power
+            };
+            lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_POWER_STATE, &pkt);
+        }
     }
 
-    SEND_RESULT(client, true);
+    if (transition) {
+        if (!SLIST_EMPTY(&targets_to_turn_on)) {
+            ok = ok && lgtd_effect_power_transition(
+                &targets_to_turn_on, LGTD_EFFECT_POWER_TRANSITION_ON, transition
+            );
+            lgtd_proto_target_list_clear(&targets_to_turn_on);
+        }
+        if (!SLIST_EMPTY(&targets_to_turn_off)) {
+            ok = ok && lgtd_effect_power_transition(
+                &targets_to_turn_off,
+                LGTD_EFFECT_POWER_TRANSITION_OFF,
+                transition
+            );
+            lgtd_proto_target_list_clear(&targets_to_turn_off);
+        }
+    }
+
+    SEND_RESULT(client, ok);
 
     lgtd_router_device_list_free(devices);
 }
 
 void
-lgtd_proto_power_off(struct lgtd_client *client,
-                     const struct lgtd_proto_target_list *targets)
-{
-    assert(targets);
-
-    struct lgtd_lifx_packet_power_state pkt = { .power = LGTD_LIFX_POWER_OFF };
-    SEND_RESULT(
-        client, lgtd_router_send(targets, LGTD_LIFX_SET_POWER_STATE, &pkt)
-    );
-}
-
-void
 lgtd_proto_set_light_from_hsbk(struct lgtd_client *client,
                                const struct lgtd_proto_target_list *targets,
                                int hue,
diff --git a/core/proto.h b/core/proto.h
--- a/core/proto.h
+++ b/core/proto.h
@@ -17,6 +17,8 @@
 
 #pragma once
 
+struct lgtd_client;
+
 struct lgtd_proto_target {
     SLIST_ENTRY(lgtd_proto_target)  link;
     char                            target[];
@@ -24,6 +26,7 @@
 SLIST_HEAD(lgtd_proto_target_list, lgtd_proto_target);
 
 void lgtd_proto_target_list_clear(struct lgtd_proto_target_list *);
+int lgtd_proto_target_list_len(const struct lgtd_proto_target_list *);
 const struct lgtd_proto_target *lgtd_proto_target_list_add(struct lgtd_client *,
                                                            struct lgtd_proto_target_list *,
                                                            const char *, int);
@@ -36,9 +39,9 @@
                              enum lgtd_lifx_waveform_type,
                              int, int, int, int,
                              int, float, int, bool);
-void lgtd_proto_power_on(struct lgtd_client *, const struct lgtd_proto_target_list *);
-void lgtd_proto_power_off(struct lgtd_client *, const struct lgtd_proto_target_list *);
-void lgtd_proto_power_toggle(struct lgtd_client *, const struct lgtd_proto_target_list *);
+void lgtd_proto_power_on(struct lgtd_client *, const struct lgtd_proto_target_list *, int);
+void lgtd_proto_power_off(struct lgtd_client *, const struct lgtd_proto_target_list *, int);
+void lgtd_proto_power_toggle(struct lgtd_client *, const struct lgtd_proto_target_list *, int);
 void lgtd_proto_get_light_state(struct lgtd_client *, const struct lgtd_proto_target_list *);
 void lgtd_proto_tag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *);
 void lgtd_proto_untag(struct lgtd_client *, const struct lgtd_proto_target_list *, const char *);
diff --git a/core/router.h b/core/router.h
--- a/core/router.h
+++ b/core/router.h
@@ -17,6 +17,8 @@
 
 #pragma once
 
+struct lgtd_lifx_tag;
+
 // TODO: return that from the functions in there and handle it:
 enum lgtd_router_error {
     LGTD_ROUTER_INVALID_TARGET_ERROR,
diff --git a/core/utils.c b/core/utils.c
--- a/core/utils.c
+++ b/core/utils.c
@@ -30,7 +30,7 @@
 #include "lightsd.h"
 
 char *
-lgtd_iee8023mactoa(const uint8_t *addr, char *buf, int buflen)
+lgtd_ieee8023mactoa(const uint8_t *addr, char *buf, int buflen)
 {
     assert(addr);
     assert(buf);
diff --git a/docs/protocol.rst b/docs/protocol.rst
--- a/docs/protocol.rst
+++ b/docs/protocol.rst
@@ -51,17 +51,27 @@
 Available methods
 -----------------
 
-.. function:: power_off(target)
+.. function:: power_off(target[, transition])
 
-   Power off the given bulb(s).
+   Power off the given bulb(s) with an optional transition.
 
-.. function:: power_on(target)
+   :param int transition: Optional time in ms it will take for the bulb to turn
+                          off.
 
-   Power on the given bulb(s).
+.. function:: power_on(target[, transition])
 
-.. function:: power_toggle(target)
+   Power on the given bulb(s) with an optional transition.
 
-   Power on (if they are off) or power off (if they are on) the given bulb(s).
+   :param int transition: Optional time in ms it will take for the bulb to turn
+                          on.
+
+.. function:: power_toggle(target[, transition])
+
+   Power on (if they are off) or power off (if they are on) the given bulb(s)
+   with an optional transition.
+
+   :param int transition: Optional time in ms it will take for the bulb to turn
+                          on off.
 
 .. function:: set_light_from_hsbk(target, hue, saturation, brightness, kelvin[, transition])
 
diff --git a/effects/CMakeLists.txt b/effects/CMakeLists.txt
new file mode 100644
--- /dev/null
+++ b/effects/CMakeLists.txt
@@ -0,0 +1,11 @@
+INCLUDE_DIRECTORIES(
+    ${CMAKE_CURRENT_SOURCE_DIR}/../
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_BINARY_DIR}/../
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+
+ADD_LIBRARY(
+    effects
+    power_transition.c
+)
diff --git a/effects/power_transition.c b/effects/power_transition.c
new file mode 100644
--- /dev/null
+++ b/effects/power_transition.c
@@ -0,0 +1,303 @@
+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
+//
+// This file is part of lighstd.
+//
+// lighstd is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// lighstd is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
+
+#include <sys/queue.h>
+#include <sys/tree.h>
+#include <assert.h>
+#include <endian.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <event2/event.h>
+
+#include "core/jsmn.h"
+#include "core/jsonrpc.h"
+#include "core/client.h"
+#include "core/time_monotonic.h"
+#include "core/timer.h"
+#include "lifx/wire_proto.h"
+#include "lifx/bulb.h"
+#include "core/proto.h"
+#include "core/router.h"
+#include "core/effect.h"
+#include "core/lightsd.h"
+#include "power_transition.h"
+
+static void
+lgtd_effect_power_transition_clear_target_list(
+                    struct lgtd_effect_power_transition_target_list *targets)
+{
+    assert(targets);
+
+    while (!SLIST_EMPTY(targets)) {
+        struct lgtd_effect_power_transition_target *target;
+        target = SLIST_FIRST(targets);
+        SLIST_REMOVE_HEAD(targets, link);
+        free(target);
+    }
+}
+
+static struct lgtd_lifx_bulb *
+lgtd_effect_power_transition_off_possibly_get_bulb(uint8_t *device_id)
+{
+    assert(device_id);
+
+    struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_get(device_id);
+    if (!bulb) {
+        char addr[LGTD_LIFX_ADDR_LENGTH];
+        LGTD_IEEE8023MACTOA(bulb->addr, addr);
+        lgtd_warn(
+            "bulb %s is unavailable: can't restore its original brightness "
+            "at the end of a power_off transition", addr
+        );
+    }
+    return bulb;
+}
+
+static void
+lgtd_effect_power_transition_off_duration_callback(
+                    const struct lgtd_effect *effect)
+{
+    assert(effect);
+
+    struct lgtd_effect_power_transition_ctx *ctx = effect->ctx.as_ptr;
+
+    struct lgtd_effect_power_transition_target *target;
+    SLIST_FOREACH(target, &ctx->targets, link) {
+        struct lgtd_lifx_bulb *bulb;
+        uint8_t *device_id = target->device_id;
+        bulb = lgtd_effect_power_transition_off_possibly_get_bulb(device_id);
+        if (bulb) { // restore the original brightness
+            struct lgtd_lifx_packet_light_color pkt_light_color = {
+                .hue = bulb->state.hue,
+                .saturation = bulb->state.saturation,
+                .brightness = target->initial_brightness,
+                .kelvin = bulb->state.kelvin
+            };
+            lgtd_lifx_wire_encode_light_color(&pkt_light_color);
+            lgtd_router_send_to_device(
+                bulb, LGTD_LIFX_SET_LIGHT_COLOR, &pkt_light_color
+            );
+        }
+    }
+
+    lgtd_effect_power_transition_clear_target_list(&ctx->targets);
+    free(ctx);
+}
+
+static void
+lgtd_effect_power_transition_off_set_brightness_to_zero(
+                    struct lgtd_effect_power_transition_target_list *targets,
+                    int duration)
+{
+    assert(targets);
+
+    struct lgtd_effect_power_transition_target *target;
+    SLIST_FOREACH(target, targets, link) {
+        // we're using LGTD_TIMER_ACTIVATE_NOW so we'll go through the event
+        // loop again which means we must check that the bulb is still there,
+        // even though this is gonna be executed right away to avoid any race
+        // condition:
+        struct lgtd_lifx_bulb *bulb;
+        uint8_t *device_id = target->device_id;
+        bulb = lgtd_effect_power_transition_off_possibly_get_bulb(device_id);
+        struct lgtd_lifx_packet_light_color pkt = {
+            .hue = bulb->state.hue,
+            .saturation = bulb->state.saturation,
+            .brightness = 0,
+            .kelvin = bulb->state.kelvin,
+            .transition = duration
+        };
+        lgtd_lifx_wire_encode_light_color(&pkt);
+        lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_LIGHT_COLOR, &pkt);
+    }
+}
+
+static void
+lgtd_effect_power_transition_off_power_off(
+                    struct lgtd_effect_power_transition_target_list *targets)
+{
+    assert(targets);
+
+    struct lgtd_effect_power_transition_target *target;
+    SLIST_FOREACH(target, targets, link) {
+        struct lgtd_lifx_bulb *bulb;
+        uint8_t *device_id = target->device_id;
+        bulb = lgtd_effect_power_transition_off_possibly_get_bulb(device_id);
+        if (bulb) {
+            struct lgtd_lifx_packet_power pkt = {
+                .power = LGTD_LIFX_POWER_OFF
+            };
+            lgtd_router_send_to_device(bulb, LGTD_LIFX_SET_POWER_STATE, &pkt);
+        }
+    }
+}
+
+static void
+lgtd_effect_power_transition_off_apply_callback(const struct lgtd_effect *effect)
+{
+    assert(effect);
+
+    struct lgtd_effect_power_transition_ctx *ctx = effect->ctx.as_ptr;
+
+    switch (effect->apply_cnt) {
+    case 0:
+        lgtd_effect_power_transition_off_set_brightness_to_zero(
+            &ctx->targets, effect->duration
+        );
+        break;
+    case 1:
+        lgtd_effect_power_transition_off_power_off(&ctx->targets);
+        break;
+    default:
+        // we'll end up there if the transition was less long than
+        // LGTD_EFFECT_POWER_TRANSITION_OFF_RESTORE_LIGHT_COLOR_DELAY_MSECS
+        break;
+    }
+}
+
+static void
+lgtd_effect_power_transition_on_apply_callback(const struct lgtd_effect *effect)
+{
+    assert(effect);
+
+    struct lgtd_effect_power_transition_ctx *ctx = effect->ctx.as_ptr;
+
+    struct lgtd_effect_power_transition_target *target;
+    SLIST_FOREACH(target, &ctx->targets, link) {
+        struct lgtd_lifx_bulb *bulb = lgtd_lifx_bulb_get(target->device_id);
+        assert(bulb);
+
+        if (bulb->state.power) {
+            continue; // skip the bulb since it's already on
+        }
+
+        struct lgtd_lifx_packet_light_color pkt_set_brightness_to_zero = {
+            .hue = bulb->state.hue,
+            .saturation = bulb->state.saturation,
+            .brightness = 0,
+            .kelvin = bulb->state.kelvin
+        };
+        lgtd_lifx_wire_encode_light_color(&pkt_set_brightness_to_zero);
+        lgtd_router_send_to_device(
+            bulb, LGTD_LIFX_SET_LIGHT_COLOR, &pkt_set_brightness_to_zero
+        );
+
+        struct lgtd_lifx_packet_power pkt_power_on = {
+            .power = LGTD_LIFX_POWER_ON
+        };
+        lgtd_router_send_to_device(
+            bulb, LGTD_LIFX_SET_POWER_STATE, &pkt_power_on
+        );
+
+        struct lgtd_lifx_packet_light_color pkt_fade_out = {
+            .hue = bulb->state.hue,
+            .saturation = bulb->state.saturation,
+            .brightness = target->initial_brightness,
+            .kelvin = bulb->state.kelvin,
+            .transition = ctx->duration
+        };
+        lgtd_lifx_wire_encode_light_color(&pkt_fade_out);
+        lgtd_router_send_to_device(
+            bulb, LGTD_LIFX_SET_LIGHT_COLOR, &pkt_fade_out
+        );
+    }
+
+    lgtd_effect_power_transition_clear_target_list(&ctx->targets);
+    free(ctx);
+}
+
+const struct lgtd_effect *
+lgtd_effect_power_transition(const struct lgtd_proto_target_list *targets,
+                             enum lgtd_effect_power_transition_type state,
+                             int duration)
+{
+    assert(targets);
+    assert(duration >= 0);
+
+    struct lgtd_effect_power_transition_ctx *ctx = NULL;
+    struct lgtd_router_device_list *devices = NULL;
+
+    devices = lgtd_router_targets_to_devices(targets);
+    if (!devices) {
+        goto error;
+    }
+
+    ctx = calloc(1, sizeof(*ctx));
+    if (!ctx) {
+        goto error;
+    }
+    ctx->duration = duration;
+
+    while (!SLIST_EMPTY(devices)) {
+        struct lgtd_router_device *device = SLIST_FIRST(devices);
+        struct lgtd_lifx_bulb *bulb = device->device;
+
+        struct lgtd_effect_power_transition_target *target;
+        target = calloc(1, sizeof(*target));
+        if (!target) {
+            goto error;
+        }
+        memcpy(target->device_id, bulb->addr, sizeof(target->device_id));
+        target->initial_brightness = bulb->state.brightness;
+        SLIST_INSERT_HEAD(&ctx->targets, target, link);
+
+        SLIST_REMOVE_HEAD(devices, link);
+        free(device);
+    }
+    free(devices);
+
+    const char *name = NULL;
+    void (*duration_cb)(const struct lgtd_effect *) = NULL;
+    int timer_flags = 0, timer_ms = 0;
+    void (*apply_cb)(const struct lgtd_effect *) = NULL;
+    if (state == LGTD_EFFECT_POWER_TRANSITION_OFF) {
+        name = "power_transition[off]";
+        apply_cb = lgtd_effect_power_transition_off_apply_callback;
+        timer_flags = LGTD_TIMER_ACTIVATE_NOW|LGTD_TIMER_PERSISTENT;
+        timer_ms = duration;
+        duration +=
+            LGTD_EFFECT_POWER_TRANSITION_OFF_RESTORE_LIGHT_COLOR_DELAY_MSECS;
+        duration_cb = lgtd_effect_power_transition_off_duration_callback;
+    } else {
+        name = "power_transition[on]";
+        apply_cb = lgtd_effect_power_transition_on_apply_callback;
+    }
+    union lgtd_effect_ctx effect_ctx = { .as_ptr = ctx };
+    const struct lgtd_effect *effect = lgtd_effect_start(
+        name, duration, duration_cb, timer_flags, timer_ms, apply_cb, effect_ctx
+    );
+    if (effect) {
+        return effect;
+    }
+
+error:
+    lgtd_warn(
+        "can't start effect power_transition[%s]",
+        state == LGTD_EFFECT_POWER_TRANSITION_OFF ? "off" : "on"
+    );
+    if (ctx) {
+        lgtd_effect_power_transition_clear_target_list(&ctx->targets);
+    }
+    if (devices) {
+        lgtd_router_device_list_free(devices);
+    }
+    return NULL;
+}
diff --git a/effects/power_transition.h b/effects/power_transition.h
new file mode 100644
--- /dev/null
+++ b/effects/power_transition.h
@@ -0,0 +1,48 @@
+// Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
+//
+// This file is part of lighstd.
+//
+// lighstd is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// lighstd is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with lighstd.  If not, see <http://www.gnu.org/licenses/>.
+
+#pragma once
+
+enum lgtd_effect_power_transition_type {
+    LGTD_EFFECT_POWER_TRANSITION_OFF,
+    LGTD_EFFECT_POWER_TRANSITION_ON,
+};
+
+// if you power off a bulb and set its color right away you'll see the bulb
+// flicker, so let's delay the restore for a bit once the transition is done:
+enum {
+    LGTD_EFFECT_POWER_TRANSITION_OFF_RESTORE_LIGHT_COLOR_DELAY_MSECS = 250
+};
+
+struct lgtd_effect_power_transition_target {
+    SLIST_ENTRY(lgtd_effect_power_transition_target)    link;
+    uint8_t                                             device_id[LGTD_LIFX_ADDR_LENGTH];
+    uint16_t                                            initial_brightness;
+};
+SLIST_HEAD(
+    lgtd_effect_power_transition_target_list,
+    lgtd_effect_power_transition_target
+);
+
+struct lgtd_effect_power_transition_ctx {
+    struct lgtd_effect_power_transition_target_list targets;
+    int                                             duration;
+};
+
+const struct lgtd_effect *lgtd_effect_power_transition(const struct lgtd_proto_target_list *,
+                                                       enum lgtd_effect_power_transition_type,
+                                                       int);
diff --git a/examples/lightsc.py b/examples/lightsc.py
--- a/examples/lightsc.py
+++ b/examples/lightsc.py
@@ -199,14 +199,18 @@
             transient=transient
         )
 
-    def power_on(self, target):
-        return self._jsonrpc_call("power_on", {"target": target})
+    def _power_call(self, call, target, transition):
+        args = [target, transition] if transition is not None else [target]
+        self._jsonrpc_call(call, args)
 
-    def power_off(self, target):
-        return self._jsonrpc_call("power_off", {"target": target})
+    def power_on(self, target, transition=None):
+        return self._power_call("power_on", target, transition)
 
-    def power_toggle(self, target):
-        return self._jsonrpc_call("power_toggle", {"target": target})
+    def power_off(self, target, transition=None):
+        return self._power_call("power_off", target, transition)
+
+    def power_toggle(self, target, transition=None):
+        return self._power_call("power_toggle", target, transition)
 
     def get_light_state(self, target):
         return self._jsonrpc_call("get_light_state", [target])
diff --git a/lifx/wire_proto.c b/lifx/wire_proto.c
--- a/lifx/wire_proto.c
+++ b/lifx/wire_proto.c
@@ -205,6 +205,24 @@
         },
         {
             REQUEST_ONLY,
+            // like SET_POWER_STATE but with transition support, not supported
+            // by the earliest versions of the bulbs (Original 1000 fw 1.5 has
+            // it, I don't know which version between 1.1 and 1.5 added it):
+            .name = "SET_POWER",
+            .type = LGTD_LIFX_SET_POWER,
+            .size = sizeof(struct lgtd_lifx_packet_power),
+            .encode = ENCODER(lgtd_lifx_wire_encode_power)
+        },
+        {
+            RESPONSE_ONLY,
+            .name = "STATE_POWER", // same thing as POWER_STATE
+            .type = LGTD_LIFX_STATE_POWER,
+            .size = sizeof(struct lgtd_lifx_packet_power_state),
+            .decode = DECODER(lgtd_lifx_wire_decode_power_state),
+            .handle = HANDLER(lgtd_lifx_gateway_handle_power_state)
+        },
+        {
+            REQUEST_ONLY,
             .name = "SET_TAGS",
             .type = LGTD_LIFX_SET_TAGS,
             .size = sizeof(struct lgtd_lifx_packet_tags),
@@ -559,16 +577,6 @@
         },
         {
             UNIMPLEMENTED,
-            .name = "SET_POWER",
-            .type = LGTD_LIFX_SET_POWER
-        },
-        {
-            UNIMPLEMENTED,
-            .name = "STATE_POWER",
-            .type = LGTD_LIFX_STATE_POWER
-        },
-        {
-            UNIMPLEMENTED,
             .name = "SET_WAVEFORM_OPTIONAL",
             .type = LGTD_LIFX_SET_WAVEFORM_OPTIONAL
         },
@@ -941,8 +949,7 @@
     pkt->brightness = le16toh(pkt->brightness);
     pkt->kelvin = le16toh(pkt->kelvin);
     pkt->dim = le16toh(pkt->dim);
-    // The bulbs actually return power values between 0 and 0xffff, not sure
-    // what the intermediate values mean, let's pull them down to 0:
+    // see comment in lgtd_lifx_wire_decode_power_state:
     if (pkt->power != LGTD_LIFX_POWER_ON) {
         pkt->power = LGTD_LIFX_POWER_OFF;
     }
@@ -954,12 +961,26 @@
 {
     assert(pkt);
 
+    // POWER_STATE does uses the full 0-65535 range and its value isn't related
+    // to the brightness or the power level, but is instead related to the
+    // transition in SET_POWER: if you do SET_POWER with a 4s transition at t 0s
+    // POWER_STATE will be 0 at t 2s StatePower will be 32767 and at t 4s
+    // POWER_STATE will be 65535 not matter what the targeted brightness is.
+    // See GH-5: https://github.com/lopter/lightsd/issues/5
     if (pkt->power != LGTD_LIFX_POWER_ON) {
         pkt->power = LGTD_LIFX_POWER_OFF;
     }
 }
 
 void
+lgtd_lifx_wire_encode_power(struct lgtd_lifx_packet_power *pkt)
+{
+    assert(pkt);
+
+    pkt->transition = htole32(pkt->transition);
+}
+
+void
 lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *pkt)
 {
     assert(pkt);
diff --git a/lifx/wire_proto.h b/lifx/wire_proto.h
--- a/lifx/wire_proto.h
+++ b/lifx/wire_proto.h
@@ -264,6 +264,11 @@
     LGTD_LIFX_POWER_ON = 0xffff
 };
 
+struct lgtd_lifx_packet_power {
+    uint16_t    power; // see enum lgtd_lifx_power_state
+    uint32le_t  transition; // msecs
+};
+
 struct lgtd_lifx_packet_power_state {
     uint16_t    power; // see enum lgtd_lifx_power_state
 };
@@ -468,6 +473,7 @@
 void lgtd_lifx_wire_decode_light_status(struct lgtd_lifx_packet_light_status *);
 void lgtd_lifx_wire_encode_light_status(struct lgtd_lifx_packet_light_status *);
 void lgtd_lifx_wire_decode_power_state(struct lgtd_lifx_packet_power_state *);
+void lgtd_lifx_wire_encode_power(struct lgtd_lifx_packet_power *);
 
 void lgtd_lifx_wire_encode_light_color(struct lgtd_lifx_packet_light_color *);
 void lgtd_lifx_wire_encode_waveform(struct lgtd_lifx_packet_waveform *);
diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt
--- a/tests/core/CMakeLists.txt
+++ b/tests/core/CMakeLists.txt
@@ -2,10 +2,12 @@
     ${LIGHTSD_SOURCE_DIR}
     ${LIGHTSD_SOURCE_DIR}/core/
     ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/../
     ${CMAKE_CURRENT_SOURCE_DIR}/../lifx
     ${LIGHTSD_BINARY_DIR}
     ${LIGHTSD_BINARY_DIR}/core/
     ${CMAKE_CURRENT_BINARY_DIR}
+    ${CMAKE_CURRENT_BINARY_DIR}/../
     ${CMAKE_CURRENT_BINARY_DIR}/../lifx
 )
 
diff --git a/tests/core/effect/CMakeLists.txt b/tests/core/effect/CMakeLists.txt
new file mode 100644
--- /dev/null
+++ b/tests/core/effect/CMakeLists.txt
@@ -0,0 +1,13 @@
+INCLUDE_DIRECTORIES(
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+
+FUNCTION(ADD_EFFECT_TEST TEST_SOURCE)
+    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} ${TIME_MONOTONIC_LIBRARY})
+ENDFUNCTION()
+
+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
+FOREACH(TEST ${TESTS})
+    ADD_EFFECT_TEST(${TEST})
+ENDFOREACH()
diff --git a/tests/core/effect/test_effect_start.c b/tests/core/effect/test_effect_start.c
new file mode 100644
--- /dev/null
+++ b/tests/core/effect/test_effect_start.c
@@ -0,0 +1,38 @@
+#include "core/effect.c"
+
+#include "mock_log.h"
+#include "mock_timer.h"
+
+union lgtd_effect_ctx TEST_EFFECT_CTX = { .as_uint = 0 };
+
+enum {
+    FINITE_WITHOUT_CALLBACK,    // duration != 0
+    FINITE_WITH_CALLBACK,
+    INSTANT,    // duration == 0
+    PERIODIC,   // timer_ms != 0
+    ONE_SHOT,   // timer_ms == 0
+} test_case = 0;;
+
+static int test_apply_callback_call_count = 0;
+
+static void
+test_apply_callback(const struct lgtd_effect *effect)
+{
+    test_apply_callback_call_count++;
+}
+
+
+int
+main(void)
+{
+    switch (test_case++) {
+        case FINITE_WITH_CALLBACK:
+            lgtd_effect_start(
+                "test", 420, NULL, 0, 0, test_apply_callback, TEST_EFFECT_CTX
+            );
+        default:
+            break;
+    }
+
+    return 0;
+}
diff --git a/tests/core/jsonrpc/test_jsonrpc_batch.c b/tests/core/jsonrpc/test_jsonrpc_batch.c
--- a/tests/core/jsonrpc/test_jsonrpc_batch.c
+++ b/tests/core/jsonrpc/test_jsonrpc_batch.c
@@ -12,12 +12,17 @@
 
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     if (!client) {
         errx(1, "missing client!");
     }
 
+    if (transition) {
+        errx(1, "transition=%d (expected 0)", transition);
+    }
+
     if (strcmp(SLIST_FIRST(targets)->target, "*")) {
         errx(
             1, "Invalid target [%s] (expected=[*])",
diff --git a/tests/core/jsonrpc/test_jsonrpc_batch_notifications_only.c b/tests/core/jsonrpc/test_jsonrpc_batch_notifications_only.c
--- a/tests/core/jsonrpc/test_jsonrpc_batch_notifications_only.c
+++ b/tests/core/jsonrpc/test_jsonrpc_batch_notifications_only.c
@@ -12,12 +12,17 @@
 
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     if (!client) {
         errx(1, "missing client!");
     }
 
+    if (transition) {
+        errx(1, "proto_power_on got transition=%d (expected 0)", transition);
+    }
+
     if (strcmp(SLIST_FIRST(targets)->target, "*")) {
         errx(
             1, "Invalid target [%s] (expected=[*])",
diff --git a/tests/core/jsonrpc/test_jsonrpc_batch_one_invalid_request.c b/tests/core/jsonrpc/test_jsonrpc_batch_one_invalid_request.c
--- a/tests/core/jsonrpc/test_jsonrpc_batch_one_invalid_request.c
+++ b/tests/core/jsonrpc/test_jsonrpc_batch_one_invalid_request.c
@@ -11,12 +11,17 @@
 
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     if (!client) {
         errx(1, "missing client!");
     }
 
+    if (transition) {
+        errx(1, "proto_power_on got transition=%d (expected 0)", transition);
+    }
+
     if (strcmp(SLIST_FIRST(targets)->target, "*")) {
         errx(
             1, "Invalid target [%s] (expected=[*])",
diff --git a/tests/core/jsonrpc/test_jsonrpc_batch_one_notification.c b/tests/core/jsonrpc/test_jsonrpc_batch_one_notification.c
--- a/tests/core/jsonrpc/test_jsonrpc_batch_one_notification.c
+++ b/tests/core/jsonrpc/test_jsonrpc_batch_one_notification.c
@@ -12,12 +12,17 @@
 
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     if (!client) {
         errx(1, "missing client!");
     }
 
+    if (transition) {
+        errx(1, "proto_power_on got transition=%d (expected 0)", transition);
+    }
+
     if (strcmp(SLIST_FIRST(targets)->target, "*")) {
         errx(
             1, "Invalid target [%s] (expected=[*])",
diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off.c
@@ -12,12 +12,17 @@
 
 void
 lgtd_proto_power_off(struct lgtd_client *client,
-                     const struct lgtd_proto_target_list *targets)
+                     const struct lgtd_proto_target_list *targets,
+                     int transition)
 {
     if (!client) {
         errx(1, "missing client!");
     }
 
+    if (transition != 420) {
+        errx(1, "Invalid transition=%d (expected 420)", transition);
+    }
+
     if (strcmp(SLIST_FIRST(targets)->target, "*")) {
         errx(
             1, "Invalid target [%s] (expected=[*])",
@@ -34,7 +39,7 @@
     const char json[] = ("{"
         "\"jsonrpc\": \"2.0\","
         "\"method\": \"power_off\","
-        "\"params\": {\"target\": \"*\"},"
+        "\"params\": {\"target\": \"*\", \"transition\": 420},"
         "\"id\": \"42\""
     "}");
     int parsed = parse_json(
diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_off_missing_target.c
@@ -12,10 +12,12 @@
 
 void
 lgtd_proto_power_off(struct lgtd_client *client,
-                     const struct lgtd_proto_target_list *targets)
+                     const struct lgtd_proto_target_list *targets,
+                     int transition)
 {
     (void)targets;
     (void)client;
+    (void)transition;
     power_off_called = true;
 }
 
diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on.c
@@ -12,12 +12,17 @@
 
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     if (!client) {
         errx(1, "missing client!");
     }
 
+    if (transition != 420) {
+        errx(1, "Invalid transition=%d (expected 420)", transition);
+    }
+
     if (strcmp(SLIST_FIRST(targets)->target, "*")) {
         errx(
             1, "Invalid target [%s] (expected=[*])",
@@ -34,7 +39,7 @@
     const char json[] = ("{"
         "\"jsonrpc\": \"2.0\","
         "\"method\": \"power_on\","
-        "\"params\": {\"target\": \"*\"},"
+        "\"params\": {\"target\": \"*\", \"transition\": 420},"
         "\"id\": \"42\""
     "}");
     int parsed = parse_json(
diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_on_missing_target.c
@@ -12,10 +12,12 @@
 
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     (void)client;
     (void)targets;
+    (void)transition;
     power_on_called = true;
 }
 
diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_toggle.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_toggle.c
--- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_toggle.c
+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_power_toggle.c
@@ -12,12 +12,17 @@
 
 void
 lgtd_proto_power_toggle(struct lgtd_client *client,
-                        const struct lgtd_proto_target_list *targets)
+                        const struct lgtd_proto_target_list *targets,
+                        int transition)
 {
     if (!client) {
         errx(1, "missing client!");
     }
 
+    if (transition != 420) {
+        errx(1, "Invalid transition=%d (expected 420)", transition);
+    }
+
     if (strcmp(SLIST_FIRST(targets)->target, "*")) {
         errx(
             1, "Invalid target [%s] (expected=[*])",
@@ -34,7 +39,7 @@
     const char json[] = ("{"
         "\"jsonrpc\": \"2.0\","
         "\"method\": \"power_toggle\","
-        "\"params\": {\"target\": \"*\"},"
+        "\"params\": {\"target\": \"*\", \"transition\": 420},"
         "\"id\": \"42\""
     "}");
     int parsed = parse_json(
diff --git a/tests/core/jsonrpc/test_jsonrpc_dispatch_one_no_params.c b/tests/core/jsonrpc/test_jsonrpc_dispatch_one_no_params.c
--- a/tests/core/jsonrpc/test_jsonrpc_dispatch_one_no_params.c
+++ b/tests/core/jsonrpc/test_jsonrpc_dispatch_one_no_params.c
@@ -10,10 +10,12 @@
 
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     (void)client;
     (void)targets;
+    (void)transition;
 
     errx(1, "lgtd_proto_power_on shouldn't have been called");
 }
diff --git a/tests/core/jsonrpc/test_jsonrpc_extract_values_from_schema_and_array_honors_objsize.c b/tests/core/jsonrpc/test_jsonrpc_extract_values_from_schema_and_array_honors_objsize.c
--- a/tests/core/jsonrpc/test_jsonrpc_extract_values_from_schema_and_array_honors_objsize.c
+++ b/tests/core/jsonrpc/test_jsonrpc_extract_values_from_schema_and_array_honors_objsize.c
@@ -28,7 +28,7 @@
             "target",
             offsetof(struct lgtd_jsonrpc_target_args, target),
             offsetof(struct lgtd_jsonrpc_target_args, target_ntokens),
-            lgtd_jsonrpc_type_string_number_or_array,
+            lgtd_jsonrpc_type_string_integer_or_array,
             false
         ),
         LGTD_JSONRPC_NODE(
@@ -37,7 +37,7 @@
             -1,
             // this must dereference json from the what's in the token (see
             // next comment):
-            lgtd_jsonrpc_type_number,
+            lgtd_jsonrpc_type_integer,
             false
         )
     };
diff --git a/tests/core/mock_effect.h b/tests/core/mock_effect.h
new file mode 100644
--- /dev/null
+++ b/tests/core/mock_effect.h
@@ -0,0 +1,45 @@
+#pragma once
+
+struct lgtd_effect;
+
+union lgtd_effect_ctx {
+    uint64_t    as_uint;
+    void        *as_ptr;
+};
+
+#ifndef MOCKED_LGTD_EFFECT_STOP
+void
+lgtd_effect_stop(struct lgtd_effect *effect)
+{
+    (void)effect;
+}
+#endif
+
+#ifndef MOCKED_LGTD_EFFECT_STOP_ALL
+void
+lgtd_effect_stop_all(void)
+{
+}
+#endif
+
+#ifndef MOCKED_LGTD_EFFECT_START
+struct lgtd_effect *
+lgtd_effect_start(const char *name,
+                  int duration,
+                  void (*duration_cb)(const struct lgtd_effect *),
+                  int timer_flags,
+                  int timer_ms,
+                  void (*apply_cb)(const struct lgtd_effect *),
+                  union lgtd_effect_ctx ctx)
+{
+    (void)name;
+    (void)duration;
+    (void)duration_cb;
+    (void)timer_flags;
+    (void)timer_ms;
+    (void)apply_cb;
+    (void)ctx;
+
+    return NULL;
+}
+#endif
diff --git a/tests/core/mock_proto.h b/tests/core/mock_proto.h
--- a/tests/core/mock_proto.h
+++ b/tests/core/mock_proto.h
@@ -35,20 +35,36 @@
 #ifndef MOCKED_LGTD_PROTO_POWER_ON
 void
 lgtd_proto_power_on(struct lgtd_client *client,
-                    const struct lgtd_proto_target_list *targets)
+                    const struct lgtd_proto_target_list *targets,
+                    int transition)
 {
     (void)client;
     (void)targets;
+    (void)transition;
 }
 #endif
 
 #ifndef MOCKED_LGTD_PROTO_POWER_OFF
 void
 lgtd_proto_power_off(struct lgtd_client *client,
-                     const struct lgtd_proto_target_list *targets)
+                     const struct lgtd_proto_target_list *targets,
+                     int transition)
 {
     (void)client;
     (void)targets;
+    (void)transition;
+}
+#endif
+
+#ifndef MOCKED_LGTD_PROTO_POWER_TOGGLE
+void
+lgtd_proto_power_toggle(struct lgtd_client *client,
+                        const struct lgtd_proto_target_list *targets,
+                        int transition)
+{
+    (void)client;
+    (void)targets;
+    (void)transition;
 }
 #endif
 
@@ -110,16 +126,6 @@
 }
 #endif
 
-#ifndef MOCKED_LGTD_PROTO_POWER_TOGGLE
-void
-lgtd_proto_power_toggle(struct lgtd_client *client,
-                        const struct lgtd_proto_target_list *targets)
-{
-    (void)client;
-    (void)targets;
-}
-#endif
-
 #ifndef MOCKED_LGTD_PROTO_SET_LABEL
 void
 lgtd_proto_set_label(struct lgtd_client *client,
diff --git a/tests/core/proto/test_proto_get_light_state.c b/tests/core/proto/test_proto_get_light_state.c
--- a/tests/core/proto/test_proto_get_light_state.c
+++ b/tests/core/proto/test_proto_get_light_state.c
@@ -1,7 +1,9 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
+#include "mock_effect.h"
 #include "mock_gateway.h"
 #include "mock_event2.h"
 #include "mock_log.h"
diff --git a/tests/core/proto/test_proto_get_light_state_empty_device_list.c b/tests/core/proto/test_proto_get_light_state_empty_device_list.c
--- a/tests/core/proto/test_proto_get_light_state_empty_device_list.c
+++ b/tests/core/proto/test_proto_get_light_state_empty_device_list.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_get_light_state_label_overflow.c b/tests/core/proto/test_proto_get_light_state_label_overflow.c
--- a/tests/core/proto/test_proto_get_light_state_label_overflow.c
+++ b/tests/core/proto/test_proto_get_light_state_label_overflow.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_gateway.h"
diff --git a/tests/core/proto/test_proto_get_light_state_null_device_list.c b/tests/core/proto/test_proto_get_light_state_null_device_list.c
--- a/tests/core/proto/test_proto_get_light_state_null_device_list.c
+++ b/tests/core/proto/test_proto_get_light_state_null_device_list.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c b/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c
--- a/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c
+++ b/tests/core/proto/test_proto_get_light_state_unknown_tag_id.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_gateway.h"
diff --git a/tests/core/proto/test_proto_power_off.c b/tests/core/proto/test_proto_power_off.c
--- a/tests/core/proto/test_proto_power_off.c
+++ b/tests/core/proto/test_proto_power_off.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
@@ -60,7 +61,7 @@
     struct lgtd_client *client;
     client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT);
 
-    lgtd_proto_power_off(client, targets);
+    lgtd_proto_power_off(client, targets, 0);
 
     return 0;
 }
diff --git a/tests/core/proto/test_proto_power_off_routing_error.c b/tests/core/proto/test_proto_power_off_routing_error.c
--- a/tests/core/proto/test_proto_power_off_routing_error.c
+++ b/tests/core/proto/test_proto_power_off_routing_error.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
@@ -60,7 +61,7 @@
     struct lgtd_client *client;
     client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT);
 
-    lgtd_proto_power_off(client, targets);
+    lgtd_proto_power_off(client, targets, 0);
 
     return 0;
 }
diff --git a/tests/core/proto/test_proto_power_on.c b/tests/core/proto/test_proto_power_on.c
--- a/tests/core/proto/test_proto_power_on.c
+++ b/tests/core/proto/test_proto_power_on.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
@@ -60,7 +61,7 @@
     struct lgtd_client *client;
     client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT);
 
-    lgtd_proto_power_on(client, targets);
+    lgtd_proto_power_on(client, targets, 0);
 
     return 0;
 }
diff --git a/tests/core/proto/test_proto_power_on_routing_error.c b/tests/core/proto/test_proto_power_on_routing_error.c
--- a/tests/core/proto/test_proto_power_on_routing_error.c
+++ b/tests/core/proto/test_proto_power_on_routing_error.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
@@ -60,7 +61,7 @@
     struct lgtd_client *client;
     client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT);
 
-    lgtd_proto_power_on(client, targets);
+    lgtd_proto_power_on(client, targets, 0);
 
     return 0;
 }
diff --git a/tests/core/proto/test_proto_power_toggle.c b/tests/core/proto/test_proto_power_toggle.c
--- a/tests/core/proto/test_proto_power_toggle.c
+++ b/tests/core/proto/test_proto_power_toggle.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_gateway.h"
@@ -133,7 +134,7 @@
     client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT);
     struct lgtd_proto_target_list *targets = (void *)0x2a;
 
-    lgtd_proto_power_toggle(client, targets);
+    lgtd_proto_power_toggle(client, targets, 0);
 
     const char expected[] = "true";
 
diff --git a/tests/core/proto/test_proto_power_toggle_targets_to_device_fails.c b/tests/core/proto/test_proto_power_toggle_targets_to_device_fails.c
--- a/tests/core/proto/test_proto_power_toggle_targets_to_device_fails.c
+++ b/tests/core/proto/test_proto_power_toggle_targets_to_device_fails.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_gateway.h"
@@ -72,7 +73,7 @@
     client = lgtd_tests_insert_mock_client(FAKE_BUFFEREVENT);
     struct lgtd_proto_target_list *targets = (void *)0x2a;
 
-    lgtd_proto_power_toggle(client, targets);
+    lgtd_proto_power_toggle(client, targets, 0);
 
     if (client_send_error_call_count != 1) {
         errx(1, "lgtd_client_send_error called %d times (expected 1)",
diff --git a/tests/core/proto/test_proto_set_label.c b/tests/core/proto/test_proto_set_label.c
--- a/tests/core/proto/test_proto_set_label.c
+++ b/tests/core/proto/test_proto_set_label.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_set_label_too_long.c b/tests/core/proto/test_proto_set_label_too_long.c
--- a/tests/core/proto/test_proto_set_label_too_long.c
+++ b/tests/core/proto/test_proto_set_label_too_long.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_set_light_from_hsbk.c b/tests/core/proto/test_proto_set_light_from_hsbk.c
--- a/tests/core/proto/test_proto_set_light_from_hsbk.c
+++ b/tests/core/proto/test_proto_set_light_from_hsbk.c
@@ -2,6 +2,7 @@
 
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c
--- a/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c
+++ b/tests/core/proto/test_proto_set_light_from_hsbk_on_routing_error.c
@@ -2,6 +2,7 @@
 
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_set_waveform.c b/tests/core/proto/test_proto_set_waveform.c
--- a/tests/core/proto/test_proto_set_waveform.c
+++ b/tests/core/proto/test_proto_set_waveform.c
@@ -2,6 +2,7 @@
 
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_set_waveform_on_routing_error.c b/tests/core/proto/test_proto_set_waveform_on_routing_error.c
--- a/tests/core/proto/test_proto_set_waveform_on_routing_error.c
+++ b/tests/core/proto/test_proto_set_waveform_on_routing_error.c
@@ -2,6 +2,7 @@
 
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_event2.h"
diff --git a/tests/core/proto/test_proto_tag_create.c b/tests/core/proto/test_proto_tag_create.c
--- a/tests/core/proto/test_proto_tag_create.c
+++ b/tests/core/proto/test_proto_tag_create.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
diff --git a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
--- a/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
+++ b/tests/core/proto/test_proto_tag_create_lifx_gw_tag_ids_full.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
diff --git a/tests/core/proto/test_proto_tag_update.c b/tests/core/proto/test_proto_tag_update.c
--- a/tests/core/proto/test_proto_tag_update.c
+++ b/tests/core/proto/test_proto_tag_update.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #define MOCKED_LIFX_GATEWAY_SEND_TO_SITE
diff --git a/tests/core/proto/test_proto_untag.c b/tests/core/proto/test_proto_untag.c
--- a/tests/core/proto/test_proto_untag.c
+++ b/tests/core/proto/test_proto_untag.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_gateway.h"
diff --git a/tests/core/proto/test_proto_untag_tag_does_not_exist.c b/tests/core/proto/test_proto_untag_tag_does_not_exist.c
--- a/tests/core/proto/test_proto_untag_tag_does_not_exist.c
+++ b/tests/core/proto/test_proto_untag_tag_does_not_exist.c
@@ -1,5 +1,6 @@
 #include "proto.c"
 
+#include "effects/mock_all.h"
 #include "mock_client_buf.h"
 #include "mock_daemon.h"
 #include "mock_gateway.h"
diff --git a/tests/effects/mock_all.h b/tests/effects/mock_all.h
new file mode 100644
--- /dev/null
+++ b/tests/effects/mock_all.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#ifndef MOCKED_LGTD_EFFECT_POWER_TRANSITION
+const struct lgtd_effect *
+lgtd_effect_power_transition(const struct lgtd_proto_target_list *targets,
+                             enum lgtd_effect_power_transition_type state,
+                             int duration)
+{
+    (void)targets;
+    (void)state;
+    (void)duration;
+
+    return NULL;
+}
+#endif