changeset 461:b19134003e8f

Finish the mq so we can sort-out the last details of the CI setup
author Louis Opter <kalessin@kalessin.fr>
date Sat, 28 May 2016 19:00:30 -0700
parents 6547b28d9983
children cd963f484771
files add_make_release.patch docs_setup_alabaster_and_ga.patch dont_use_ev_assign.patch fix_freebsd_build.patch network_discovery.patch optional_jsonrpc_args.patch series white_colors_clarifications.patch
diffstat 8 files changed, 0 insertions(+), 4030 deletions(-) [+]
line wrap: on
line diff
--- a/add_make_release.patch	Sat May 28 18:42:04 2016 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1622 +0,0 @@
-# HG changeset patch
-# Parent  4b74a5b40d1d3fdeab6a639975ae941544839e33
-Monorepo the packages and setup continuous integration
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -3,10 +3,8 @@
- 
- PROJECT(LIGHTSD C)
- 
--SET(CPACK_PACKAGE_VERSION_MAJOR "1")
--SET(CPACK_PACKAGE_VERSION_MINOR "1")
--SET(CPACK_PACKAGE_VERSION_PATCH "2")
--SET(LIGHTSD_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
-+SET(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${LIGHTSD_SOURCE_DIR}/CMakeScripts")
-+INCLUDE(LightsdVersion)
- 
- MESSAGE(STATUS "lightsd version: ${LIGHTSD_VERSION}")
- MESSAGE(STATUS "CMake version: ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}.${CMAKE_PATCH_VERSION}")
-@@ -15,8 +13,6 @@
- MESSAGE(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}")
- MESSAGE(STATUS "Source directory: ${LIGHTSD_SOURCE_DIR}")
- 
--SET(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${LIGHTSD_SOURCE_DIR}/CMakeScripts")
--
- ENABLE_TESTING()
- 
- ### Platform checks ############################################################
-@@ -26,6 +22,14 @@
- FIND_PACKAGE(Endian REQUIRED)
- FIND_PACKAGE(Sphinx)
- 
-+# Release tools:
-+FIND_PACKAGE(Gzip)
-+FIND_PACKAGE(Hg)
-+FIND_PACKAGE(PythonInterp 3.3) # >= 3.3 for os.replace
-+FIND_PACKAGE(BsdTar)
-+FIND_PACKAGE(Virtualenv)
-+FIND_PACKAGE(Xz)
-+
- INCLUDE(CheckFunctionExists)
- INCLUDE(CheckVariableExists)
- INCLUDE(TestBigEndian)
-@@ -92,9 +96,7 @@
- 
- # 2.8.11 is the first version with TARGET_INCLUDE_DIRECTORIES:
- IF (CMAKE_VERSION VERSION_GREATER 2.8.10)
--    CONFIGURE_FILE(
--        CTestCustom.cmake.in "${LIGHTSD_BINARY_DIR}/CTestCustom.cmake" @ONLY
--    )
-+    CONFIGURE_FILE(CTestCustom.cmake.in "${LIGHTSD_BINARY_DIR}/CTestCustom.cmake" @ONLY)
-     ADD_SUBDIRECTORY(tests)
- ELSE ()
-     MESSAGE(
-@@ -105,7 +107,34 @@
- ENDIF ()
- 
- IF (SPHINX_FOUND)
-+    MESSAGE(STATUS "Sphinx found, docs generation enabled")
-     ADD_SUBDIRECTORY(docs)
-+ELSE ()
-+    MESSAGE(STATUS "Shpinx wasn't found, docs generation disabled")
-+ENDIF ()
-+
-+IF (
-+    PYTHONINTERP_FOUND
-+    AND PYTHON_VERSION_MAJOR EQUAL 3
-+    AND PYTHON_VERSION_MINOR GREATER 2
-+    AND VIRTUALENV_FOUND
-+    AND HG_FOUND
-+    AND BSDTAR_FOUND
-+    AND GZIP_FOUND
-+    AND XZ_FOUND
-+)
-+    MESSAGE(
-+        STATUS
-+        "Python >= 3.3, virtualenv, bsdtar, gzip, xz, and mercurial (hg) "
-+        "found, release commands enabled"
-+    )
-+    ADD_SUBDIRECTORY(dist)
-+ELSE ()
-+    MESSAGE(
-+        STATUS
-+        "Python >= 3.3 and/or virtualenv, bsdtar, gzip, xz, mercurial (hg) "
-+        "weren't found, release commands disabled"
-+    )
- ENDIF ()
- 
- INSTALL(
-diff --git a/CMakeScripts/FindBsdTar.cmake b/CMakeScripts/FindBsdTar.cmake
-new file mode 100644
---- /dev/null
-+++ b/CMakeScripts/FindBsdTar.cmake
-@@ -0,0 +1,6 @@
-+FIND_PROGRAM(
-+    BSDTAR_EXECUTABLE NAMES bsdtar
-+    DOC "Path to the bsdtar executable"
-+)
-+
-+FIND_PACKAGE_HANDLE_STANDARD_ARGS(BsdTar DEFAULT_MSG BSDTAR_EXECUTABLE)
-diff --git a/CMakeScripts/FindGzip.cmake b/CMakeScripts/FindGzip.cmake
-new file mode 100644
---- /dev/null
-+++ b/CMakeScripts/FindGzip.cmake
-@@ -0,0 +1,6 @@
-+FIND_PROGRAM(
-+    GZIP_EXECUTABLE NAMES gzip
-+    DOC "Path to the gzip executable"
-+)
-+
-+FIND_PACKAGE_HANDLE_STANDARD_ARGS(Gzip DEFAULT_MSG GZIP_EXECUTABLE)
-diff --git a/CMakeScripts/FindSphinx.cmake b/CMakeScripts/FindSphinx.cmake
---- a/CMakeScripts/FindSphinx.cmake
-+++ b/CMakeScripts/FindSphinx.cmake
-@@ -1,7 +1,7 @@
- FIND_PROGRAM(
-     SPHINX_EXECUTABLE
-     NAMES sphinx-build sphinx-build2
--    DOC "Path to sphinx-build executable"
-+    DOC "Path to the sphinx-build executable"
- )
- 
- FIND_PACKAGE_HANDLE_STANDARD_ARGS(Sphinx DEFAULT_MSG SPHINX_EXECUTABLE)
-diff --git a/CMakeScripts/FindVirtualenv.cmake b/CMakeScripts/FindVirtualenv.cmake
-new file mode 100644
---- /dev/null
-+++ b/CMakeScripts/FindVirtualenv.cmake
-@@ -0,0 +1,7 @@
-+FIND_PROGRAM(
-+    VIRTUALENV_EXECUTABLE
-+    NAMES virtualenv virtualenv2
-+    DOC "Path to the virtualenv executable"
-+)
-+
-+FIND_PACKAGE_HANDLE_STANDARD_ARGS(Virtualenv DEFAULT_MSG VIRTUALENV_EXECUTABLE)
-diff --git a/CMakeScripts/FindXz.cmake b/CMakeScripts/FindXz.cmake
-new file mode 100644
---- /dev/null
-+++ b/CMakeScripts/FindXz.cmake
-@@ -0,0 +1,6 @@
-+FIND_PROGRAM(
-+    XZ_EXECUTABLE NAMES xz
-+    DOC "Path to the xz executable"
-+)
-+
-+FIND_PACKAGE_HANDLE_STANDARD_ARGS(Xz DEFAULT_MSG XZ_EXECUTABLE)
-diff --git a/CMakeScripts/LightsdVersion.cmake b/CMakeScripts/LightsdVersion.cmake
-new file mode 100644
---- /dev/null
-+++ b/CMakeScripts/LightsdVersion.cmake
-@@ -0,0 +1,7 @@
-+# NOTE: auto-generated by the release target
-+SET(CPACK_PACKAGE_VERSION_MAJOR "1")
-+SET(CPACK_PACKAGE_VERSION_MINOR "2")
-+SET(CPACK_PACKAGE_VERSION_PATCH "0")
-+SET(LIGHTSD_PRERELEASE "rc.1")
-+SET(LIGHTSD_BUILD "")
-+SET(LIGHTSD_VERSION "1.2.0-rc.1")
-diff --git a/dist/CMakeLists.txt b/dist/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/dist/CMakeLists.txt
-@@ -0,0 +1,47 @@
-+SET(VENV_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/py-env")
-+SET(VENV_PYTHON "${VENV_DIRECTORY}/bin/python")
-+SET(VENV_STAMP "${VENV_DIRECTORY}/stamp")
-+SET(PIP_REQUIREMENTS "${CMAKE_CURRENT_SOURCE_DIR}/requirements-release.txt")
-+
-+CONFIGURE_FILE(release.py.in "${CMAKE_CURRENT_BINARY_DIR}/release.py" @ONLY)
-+
-+ADD_CUSTOM_COMMAND(
-+    OUTPUT "${VENV_STAMP}"
-+    COMMAND "${VIRTUALENV_EXECUTABLE}" -q -p "${PYTHON_EXECUTABLE}" "${VENV_DIRECTORY}"
-+    COMMAND "${VENV_DIRECTORY}/bin/pip" -q install -r "${PIP_REQUIREMENTS}"
-+    COMMAND "${CMAKE_COMMAND}" -E touch "${VENV_STAMP}"
-+    DEPENDS "${PIP_REQUIREMENTS}"
-+    COMMENT "Setting up a Python virtualenv at ${VENV_DIRECTORY} for the release script"
-+    VERBATIM
-+)
-+
-+SET(
-+    RELEASE_COMMANDS
-+    release
-+    pre_release
-+    package_release
-+    release_new_tag
-+    release_docs
-+    release_debuild
-+)
-+FOREACH (TARGET ${RELEASE_COMMANDS})
-+    ADD_CUSTOM_TARGET(
-+        ${TARGET}
-+        COMMAND "${VENV_PYTHON}" "${CMAKE_CURRENT_BINARY_DIR}/release.py" "${TARGET}"
-+        DEPENDS "${VENV_STAMP}"
-+        VERBATIM
-+    )
-+ENDFOREACH ()
-+
-+ADD_DEPENDENCIES(release_docs docs)
-+
-+SET(
-+    EXTRA_OUTPUT
-+    "${CMAKE_CURRENT_BINARY_DIR}/debuild" # release_debuild
-+    "${CMAKE_CURRENT_BINARY_DIR}/lightsd-${LIGHTSD_VERSION}.tar" # release_new_tag
-+    "${VENV_DIRECTORY}"
-+    "${VENV_STAMP}"
-+)
-+SET_DIRECTORY_PROPERTIES(
-+    PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${EXTRA_OUTPUT}"
-+)
-diff --git a/dist/dpkg/.hgtags b/dist/dpkg/.hgtags
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/.hgtags
-@@ -0,0 +1,3 @@
-+11534a873dbc92fd9c7225cf37d67942f852a67c 1.1.0
-+60c3d81a6ff2305eca554c12c42d272d5f2187db 1.1.1
-+bf09addc0ea974fc378f1acf48a8255e485f87d1 1.1.2
-diff --git a/dist/dpkg/debian/changelog b/dist/dpkg/debian/changelog
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/changelog
-@@ -0,0 +1,29 @@
-+lightsd (1.1.2-1) unstable; urgency=low
-+
-+  * New upstream release: fix LIFX LAN protocol v2 handling.
-+
-+ -- Louis Opter <kalessin@kalessin.fr>  Mon, 30 Nov 2015 10:38:18 +0000
-+
-+lightsd (1.1.1-1) unstable; urgency=low
-+
-+  * New upstream release: responsiveness improvements.
-+
-+ -- Louis Opter <kalessin@kalessin.fr>  Tue, 17 Nov 2015 10:19:02 +0000
-+
-+lightsd (1.1.0-2) unstable; urgency=low
-+
-+  * Fix parallel builds
-+
-+ -- Louis Opter <kalessin@kalessin.fr>  Sat, 14 Nov 2015 06:35:43 +0000
-+
-+lightsd (1.1.0-1) unstable; urgency=low
-+
-+  * First published release
-+
-+ -- Louis Opter <kalessin@kalessin.fr>  Sun, 08 Nov 2015 08:06:51 +0000
-+
-+lightsd (1.0.1-1) unstable; urgency=low
-+
-+  * Initial release
-+
-+ -- Louis Opter <kalessin@kalessin.fr>  Tue, 03 Nov 2015 07:43:24 +0000
-diff --git a/dist/dpkg/debian/compat b/dist/dpkg/debian/compat
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/compat
-@@ -0,0 +1,1 @@
-+9
-diff --git a/dist/dpkg/debian/control b/dist/dpkg/debian/control
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/control
-@@ -0,0 +1,24 @@
-+Source: lightsd
-+Section: contrib/utils
-+Priority: optional
-+Maintainer: Louis Opter <kalessin@kalessin.fr>
-+Uploaders: Buildbot <buildbot@kalessin.fr>
-+Build-Depends: debhelper (>= 9), cmake (>= 2.8.9), libevent-dev (>= 2.0.19)
-+Standards-Version: 3.9.6
-+Homepage: https://github.com/lopter/lightsd/
-+Vcs-Git: https://github.com/lopter/dpkg-lightsd.git
-+Vcs-Browser: https://github.com/lopter/dpkg-lightsd/
-+
-+Package: lightsd
-+Architecture: any
-+Depends: ${shlibs:Depends}, ${misc:Depends}
-+Suggests: python3, ipython3
-+Description: Centralized daemon to control your LIFX bulbs
-+ lightsd discovers LIFX bulbs on the local network and acts as a central
-+ point of control for them via a JSON-RPC interface.
-+ .
-+ The JSON-RPC interface can run over TCP, named pipes as well as Unix
-+ sockets and allows you to retrieve the state of the bulbs or set their
-+ colors. Effects, grouping and de-grouping operations are also supported.
-+ .
-+ Visit https://docs.lightsd.io/ for more information.
-diff --git a/dist/dpkg/debian/copyright b/dist/dpkg/debian/copyright
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/copyright
-@@ -0,0 +1,62 @@
-+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-+Upstream-Name: lightsd
-+Source: https://github.com/lopter/lightsd
-+
-+Files: *
-+Copyright: 2014-2015 Louis Opter <kalessin@kalessin.fr>
-+License: GPL-3.0+
-+
-+Files: debian/*
-+Copyright: 2015 Louis Opter <kalessin@kalessin.fr>
-+License: BSD-3-clause
-+
-+Files: examples/*
-+Copyright: 2015 Louis Opter <kalessin@kalessin.fr>
-+License: BSD-3-clause
-+
-+Files: share/lightsc.sh
-+Copyright: 2015 Louis Opter <kalessin@kalessin.fr>
-+License: BSD-3-clause
-+
-+License: GPL-3.0+
-+ This program 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.
-+ .
-+ This package 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 this program. If not, see <http://www.gnu.org/licenses/>.
-+ .
-+ On Debian systems, the complete text of the GNU General
-+ Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
-+
-+License: BSD-3-clause
-+ Redistribution and use in source and binary forms, with or without
-+ modification, are permitted provided that the following conditions are met:
-+ .
-+ 1. Redistributions of source code must retain the above copyright notice, this
-+ list of conditions and the following disclaimer.
-+ .
-+ 2. Redistributions in binary form must reproduce the above copyright notice,
-+ this list of conditions and the following disclaimer in the documentation
-+ and/or other materials provided with the distribution.
-+ .
-+ 3. Neither the name of the copyright holder nor the names of its contributors
-+ may be used to endorse or promote products derived from this software without
-+ specific prior written permission.
-+ .
-+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-diff --git a/dist/dpkg/debian/docs b/dist/dpkg/debian/docs
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/docs
-@@ -0,0 +1,1 @@
-+README.rst
-diff --git a/dist/dpkg/debian/init.d b/dist/dpkg/debian/init.d
-new file mode 100755
---- /dev/null
-+++ b/dist/dpkg/debian/init.d
-@@ -0,0 +1,129 @@
-+#!/bin/sh
-+### BEGIN INIT INFO
-+# Provides:          lightsd
-+# Required-Start:    $local_fs $network $syslog $remote_fs
-+# Required-Stop:     $local_fs $network $syslog $remote_fs
-+# Default-Start:     2 3 4 5
-+# Default-Stop:      0 1 6
-+# Short-Description: Start lightsd at boot time
-+# Description:       Centralized daemon to discover and control LIFX bulbs.
-+### END INIT INFO
-+
-+# Author: Louis Opter <kalessin@kalessin.fr>
-+
-+# Do NOT "set -e"
-+
-+# PATH should only include /usr/* if it runs after the mountnfs.sh script
-+PATH=/sbin:/usr/sbin:/bin:/usr/bin
-+DESC="lightsd"
-+NAME=lightsd
-+DAEMON=/usr/bin/lightsd
-+PIDFILE=/var/run/$NAME.pid
-+SCRIPTNAME=/etc/init.d/$NAME
-+
-+# Exit if the package is not installed
-+[ -x "$DAEMON" ] || exit 0
-+
-+# Read configuration variable file if it is present
-+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
-+
-+DAEMON_ARGS="
-+    --daemonize
-+    --verbosity warning
-+    --user lightsd
-+    --pidfile $PIDFILE
-+    --socket /var/run/lightsd/socket
-+    --command-pipe /var/run/lightsd/pipe
-+    $DAEMON_OPTS
-+"
-+
-+# Load the VERBOSE setting and other rcS variables
-+. /lib/init/vars.sh
-+
-+# Define LSB log_* functions.
-+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
-+# and status_of_proc is working.
-+. /lib/lsb/init-functions
-+
-+do_start()
-+{
-+	# Return
-+	#   0 if daemon has been started
-+	#   1 if daemon was already running
-+	#   2 if daemon could not be started
-+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
-+		|| return 1
-+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_ARGS \
-+		|| return 2
-+}
-+
-+do_stop()
-+{
-+	# Return
-+	#   0 if daemon has been stopped
-+	#   1 if daemon was already stopped
-+	#   2 if daemon could not be stopped
-+	#   other if a failure occurred
-+	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
-+	RETVAL="$?"
-+	[ "$RETVAL" = 2 ] && return 2
-+	# Wait for children to finish too if this is a daemon that forks
-+	# and if the daemon is only ever run from this initscript.
-+	# If the above conditions are not satisfied then add some other code
-+	# that waits for the process to drop all resources that could be
-+	# needed by services started subsequently.  A last resort is to
-+	# sleep for some time.
-+	start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
-+	[ "$?" = 2 ] && return 2
-+	# Many daemons don't delete their pidfiles when they exit.
-+	rm -f $PIDFILE
-+	return "$RETVAL"
-+}
-+
-+case "$1" in
-+  start)
-+	[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
-+	do_start
-+	case "$?" in
-+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
-+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
-+	esac
-+	;;
-+  stop)
-+	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
-+	do_stop
-+	case "$?" in
-+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
-+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
-+	esac
-+	;;
-+  status)
-+	status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
-+	;;
-+  restart|force-reload)
-+	log_daemon_msg "Restarting $DESC" "$NAME"
-+	do_stop
-+	case "$?" in
-+	  0|1)
-+		do_start
-+		case "$?" in
-+			0) log_end_msg 0 ;;
-+			1) log_end_msg 1 ;; # Old process is still running
-+			*) log_end_msg 1 ;; # Failed to start
-+		esac
-+		;;
-+	  *)
-+		# Failed to stop
-+		log_end_msg 1
-+		;;
-+	esac
-+	;;
-+  *)
-+	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
-+	exit 3
-+	;;
-+esac
-+
-+:
-+
-+# vim: set ft=sh:
-diff --git a/dist/dpkg/debian/lightsd.default b/dist/dpkg/debian/lightsd.default
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/lightsd.default
-@@ -0,0 +1,10 @@
-+# Defaults for lightsd initscript
-+# sourced by /etc/init.d/lightsd
-+# installed at /etc/default/lightsd by the maintainer scripts
-+
-+#
-+# This is a POSIX shell fragment
-+#
-+
-+# Additional options that are passed to the Daemon.
-+DAEMON_OPTS=""
-diff --git a/dist/dpkg/debian/postinst b/dist/dpkg/debian/postinst
-new file mode 100755
---- /dev/null
-+++ b/dist/dpkg/debian/postinst
-@@ -0,0 +1,55 @@
-+#!/bin/sh
-+# postinst script for lightsd
-+#
-+# see: dh_installdeb(1)
-+
-+set -e
-+
-+# summary of how this script can be called:
-+#        * <postinst> `configure' <most-recently-configured-version>
-+#        * <old-postinst> `abort-upgrade' <new version>
-+#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
-+#          <new-version>
-+#        * <postinst> `abort-remove'
-+#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
-+#          <failed-install-package> <version> `removing'
-+#          <conflicting-package> <version>
-+# for details, see http://www.debian.org/doc/debian-policy/ or
-+# the debian-policy package
-+
-+
-+case "$1" in
-+    configure)
-+        getent group lightsd >/dev/null || groupadd -r lightsd
-+        getent passwd lightsd >/dev/null || useradd -r -d / -g lightsd lightsd
-+
-+        cat << 'EOF'
-+
-+lightsd runs under the `lightsd' user and group by default; add yourself to
-+this group to be able to open lightsd's socket and pipe under /var/run/lightsd:
-+
-+  gpasswd -a $USER lightsd
-+
-+Re-open your current desktop or ssh session for the change to take effect.
-+Then use systemctl to start lightsd; you can start playing with lightsd with:
-+
-+  `lightsd --prefix`/share/doc/lightsd/examples/lightsc.py
-+
-+EOF
-+    ;;
-+
-+    abort-upgrade|abort-remove|abort-deconfigure)
-+    ;;
-+
-+    *)
-+        echo "postinst called with unknown argument \`$1'" >&2
-+        exit 1
-+    ;;
-+esac
-+
-+# dh_installdeb will replace this with shell code automatically
-+# generated by other debhelper scripts.
-+
-+#DEBHELPER#
-+
-+exit 0
-diff --git a/dist/dpkg/debian/postrm b/dist/dpkg/debian/postrm
-new file mode 100755
---- /dev/null
-+++ b/dist/dpkg/debian/postrm
-@@ -0,0 +1,41 @@
-+#!/bin/sh
-+# postrm script for lightsd
-+#
-+# see: dh_installdeb(1)
-+
-+set -e
-+
-+# summary of how this script can be called:
-+#        * <postrm> `remove'
-+#        * <postrm> `purge'
-+#        * <old-postrm> `upgrade' <new-version>
-+#        * <new-postrm> `failed-upgrade' <old-version>
-+#        * <new-postrm> `abort-install'
-+#        * <new-postrm> `abort-install' <old-version>
-+#        * <new-postrm> `abort-upgrade' <old-version>
-+#        * <disappearer's-postrm> `disappear' <overwriter>
-+#          <overwriter-version>
-+# for details, see http://www.debian.org/doc/debian-policy/ or
-+# the debian-policy package
-+
-+
-+case "$1" in
-+    purge|remove|abort-install)
-+        getent passwd lightsd >/dev/null && userdel lightsd
-+    ;;
-+
-+    upgrade|failed-upgrade|abort-upgrade|disappear)
-+    ;;
-+
-+    *)
-+        echo "postrm called with unknown argument \`$1'" >&2
-+        exit 1
-+    ;;
-+esac
-+
-+# dh_installdeb will replace this with shell code automatically
-+# generated by other debhelper scripts.
-+
-+#DEBHELPER#
-+
-+exit 0
-diff --git a/dist/dpkg/debian/rules b/dist/dpkg/debian/rules
-new file mode 100755
---- /dev/null
-+++ b/dist/dpkg/debian/rules
-@@ -0,0 +1,48 @@
-+#!/usr/bin/make -f
-+# See debhelper(7) (uncomment to enable)
-+# output every command that modifies files on the build system.
-+DH_VERBOSE = 1
-+
-+# see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/*
-+DPKG_EXPORT_BUILDFLAGS = 1
-+include /usr/share/dpkg/default.mk
-+
-+# see FEATURE AREAS in dpkg-buildflags(1)
-+export DEB_BUILD_MAINT_OPTIONS = hardening=+all
-+
-+# see ENVIRONMENT in dpkg-buildflags(1)
-+# package maintainers to append CFLAGS
-+#export DEB_CFLAGS_MAINT_APPEND  = -Wall -pedantic
-+# package maintainers to append LDFLAGS
-+#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
-+
-+ifneq (,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
-+    NUMJOBS = $(patsubst parallel=%,%,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
-+    MAKEFLAGS += -j$(NUMJOBS)
-+endif
-+
-+ifeq (,$(wildcard /run))
-+    RUNTIME_DIRECTORY=/var/run
-+else
-+    RUNTIME_DIRECTORY=/run
-+endif
-+
-+# main packaging script based on dh7 syntax
-+%:
-+	dh $@
-+
-+# debmake generated override targets
-+# This is example for Cmake (See http://bugs.debian.org/641051 )
-+override_dh_auto_configure:
-+	dh_auto_configure --					\
-+	    	-DCMAKE_BUILD_TYPE=RELEASE			\
-+		-DCMAKE_INSTALL_PREFIX=/usr			\
-+		-DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH)	\
-+		-DLGTD_RUNTIME_DIRECTORY=$(RUNTIME_DIRECTORY)/lightsd
-+
-+override_dh_auto_build:
-+	dh_auto_build --parallel
-+
-+override_dh_auto_install:
-+	dh_auto_install
-+	rm -f debian/lightsd/usr/share/doc/lightsd/COPYING
-diff --git a/dist/dpkg/debian/source/format b/dist/dpkg/debian/source/format
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/source/format
-@@ -0,0 +1,1 @@
-+3.0 (quilt)
-diff --git a/dist/dpkg/debian/watch b/dist/dpkg/debian/watch
-new file mode 100644
---- /dev/null
-+++ b/dist/dpkg/debian/watch
-@@ -0,0 +1,7 @@
-+# See uscan(1) for format
-+
-+# Compulsory line, this is a version 3 file
-+version=3
-+
-+opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/lightsd-$1\.tar\.gz/ \
-+       https://github.com/lopter/lightsd/tags .*/?(\d+\.\d+.\d+)\.tar\.gz
-diff --git a/dist/homebrew/LICENSE b/dist/homebrew/LICENSE
-new file mode 100644
---- /dev/null
-+++ b/dist/homebrew/LICENSE
-@@ -0,0 +1,28 @@
-+Copyright (c) 2015, Louis Opter <kalessin@kalessin.fr>
-+All rights reserved.
-+
-+Redistribution and use in source and binary forms, with or without
-+modification, are permitted provided that the following conditions are met:
-+
-+* Redistributions of source code must retain the above copyright notice, this
-+  list of conditions and the following disclaimer.
-+
-+* Redistributions in binary form must reproduce the above copyright notice,
-+  this list of conditions and the following disclaimer in the documentation
-+  and/or other materials provided with the distribution.
-+
-+* Neither the name of homebrew-lightsd nor the names of its
-+  contributors may be used to endorse or promote products derived from
-+  this software without specific prior written permission.
-+
-+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-+
-diff --git a/dist/homebrew/README.rst b/dist/homebrew/README.rst
-new file mode 100644
---- /dev/null
-+++ b/dist/homebrew/README.rst
-@@ -0,0 +1,31 @@
-+Homebrew lightsd
-+================
-+
-+This `brew`_ tap let you install `lightsd`_, a daemon written C to control your
-+LIFX wifi smart bulbs, on Mac OS X.
-+
-+.. _brew: http://brew.sh
-+.. _lightsd: https://github.com/lopter/lightsd/
-+
-+Instructions
-+------------
-+
-+Make sure you have brew installed: http://brew.sh.
-+
-+::
-+
-+   brew install lopter/lightsd/lightsd
-+
-+Or:
-+
-+::
-+
-+   brew tap lopter/lightsd
-+   brew install lightsd
-+
-+Documentation
-+-------------
-+
-+Please checkout lightsd's repository: https://github.com/lopter/lightsd/.
-+
-+.. vim: set tw=80 spelllang=en spell:
-diff --git a/dist/homebrew/lightsd.rb b/dist/homebrew/lightsd.rb
-new file mode 100644
---- /dev/null
-+++ b/dist/homebrew/lightsd.rb
-@@ -0,0 +1,95 @@
-+require "formula"
-+
-+class Lightsd < Formula
-+  desc "Daemon to control your LIFX wifi smart bulbs"
-+  homepage "https://github.com/lopter/lightsd/"
-+  url "{{ archive_url  }}"
-+  sha256 "{{ archive_sha256 }}"
-+  revision {{ build_number }}
-+
-+  depends_on "cmake" => :build
-+  depends_on "libevent" => :build
-+  depends_on "python3" => :optional
-+
-+  def install
-+    # XXX, wtf? https://github.com/Homebrew/homebrew/issues/46061
-+    ENV["PATH"] = "/usr/bin:#{ENV["PATH"]}"
-+
-+    args = std_cmake_args
-+    args << "-DLGTD_RUNTIME_DIRECTORY=#{var}/run/lightsd"
-+
-+    # idk what std_cmake_args is supposed to do but it appears to be missing
-+    # proper release flags:
-+    cflags = %W[
-+      -fstack-protector-strong
-+      --param=ssp-buffer-size=4
-+      -O3
-+      -DNDEBUG
-+    ]
-+    args << "-DCMAKE_BUILD_TYPE=RELEASE"
-+    args << "-DCMAKE_C_FLAGS_RELEASE='#{cflags * " "}'"
-+
-+    system "cmake", *args
-+    system "make", "install"
-+  end
-+
-+  def plist; <<-EOS.undent
-+    <?xml version="1.0" encoding="UTF-8"?>
-+    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-+    <plist version="1.0">
-+      <dict>
-+        <key>KeepAlive</key>
-+        <dict>
-+          <key>SuccessfulExit</key>
-+          <false/>
-+        </dict>
-+        <key>Label</key>
-+        <string>#{plist_name}</string>
-+        <key>ProgramArguments</key>
-+        <array>
-+          <string>#{opt_bin}/lightsd</string>
-+          <string>-v</string>
-+          <string>warning</string>
-+          <string>-s</string>
-+          <string>#{var}/run/lightsd/socket</string>
-+          <string>-c</string>
-+          <string>#{var}/run/lightsd/pipe</string>
-+        </array>
-+        <key>RunAtLoad</key>
-+        <true/>
-+        <key>WorkingDirectory</key>
-+        <string>#{var}</string>
-+        <key>StandardErrorPath</key>
-+        <string>#{var}/log/lightsd.log</string>
-+        <key>StandardOutPath</key>
-+        <string>#{var}/log/lightsd.log</string>
-+      </dict>
-+    </plist>
-+    EOS
-+  end
-+
-+  def caveats; <<-EOS.undent
-+    Once you've started lightsd with launchctl load (see below), you can start
-+    poking around with lightsc.py:
-+
-+      `lightsd --prefix`/share/lightsd/examples/lightsc.py
-+    EOS
-+  end
-+
-+  head do
-+    url "https://github.com/lopter/lightsd.git"
-+  end
-+
-+  test do
-+    Dir.mktmpdir("lightsd-test") do |dir|
-+      args = %W[
-+        -l ::1:0
-+        -l 127.0.0.1:0
-+        -c #{dir}/lightsd.cmd
-+        -h
-+      ]
-+      system "#{bin}/lightsd", *args
-+    end
-+  end
-+end
-+
-diff --git a/dist/openwrt/utils/lightsd/Makefile b/dist/openwrt/utils/lightsd/Makefile
-new file mode 100644
---- /dev/null
-+++ b/dist/openwrt/utils/lightsd/Makefile
-@@ -0,0 +1,62 @@
-+#
-+# Copyright (C) 2015 Louis Opter <kalessin@kalessin.fr>
-+#
-+# This is free software, licensed under the GNU General Public License v2.
-+# See /LICENSE for more information.
-+#
-+
-+include $(TOPDIR)/rules.mk
-+
-+PKG_NAME:=lightsd
-+PKG_VERSION:={{ version }}
-+PKG_RELEASE:={{ build_number }}
-+PKG_MAINTAINER:=Louis Opter <kalessin@kalessin.fr>
-+PKG_LICENSE:=GPL-3.0+
-+PKG_SOURCE_URL:=https://downloads.lightsd.io/releases/
-+PKG_SOURCE:={{ archive_name }}
-+PKG_MD5SUM:={{ archive_md5 }}
-+PKG_BUILD_DIR:=$(BUILD_DIR)/lightsd-$(PKG_VERSION)
-+
-+include $(INCLUDE_DIR)/package.mk
-+include $(INCLUDE_DIR)/cmake.mk
-+
-+CMAKE_OPTIONS += \
-+	-DCMAKE_BUILD_TYPE=RELEASE \
-+	-DLGTD_RUNTIME_DIRECTORY=/var/run/lightsd
-+
-+define Package/lightsd
-+  SECTION:=utils
-+  CATEGORY:=Utilities
-+  DEPENDS:=+libevent2-core
-+  TITLE:=Daemon to control your LIFX Wi-Fi smart bulbs
-+  MAINTAINER:=Louis Opter <kalessin@kalessin.fr>
-+  URL:=https://github.com/lopter/lightsd
-+  USERID:=lightsd:lightsd
-+endef
-+
-+define Package/lightsd/install
-+	$(INSTALL_DIR) \
-+		$(1)/etc/init.d/ \
-+		$(1)/usr/bin \
-+		$(1)/usr/share/lightsd \
-+		$(1)/usr/share/doc/lightsd
-+
-+	$(INSTALL_BIN) \
-+		$(PKG_INSTALL_DIR)/usr/bin/lightsd \
-+		$(1)/usr/bin/lightsd
-+
-+	$(INSTALL_BIN) \
-+		./files/lightsd.init \
-+		$(1)/etc/init.d/lightsd
-+
-+	$(CP) \
-+		$(PKG_INSTALL_DIR)/usr/share/lightsd \
-+		$(1)/usr/share/lightsd
-+
-+	$(CP) \
-+		$(PKG_INSTALL_DIR)/usr/share/doc/lightsd \
-+		$(1)/usr/share/doc/lightsd
-+endef
-+
-+$(eval $(call BuildPackage,lightsd))
-+
-diff --git a/dist/openwrt/utils/lightsd/files/lightsd.init b/dist/openwrt/utils/lightsd/files/lightsd.init
-new file mode 100644
---- /dev/null
-+++ b/dist/openwrt/utils/lightsd/files/lightsd.init
-@@ -0,0 +1,19 @@
-+#!/bin/sh /etc/rc.common
-+# Copyright (C) 2015 Louis Opter <kalessin@kalessin.fr>
-+#
-+# This is free software, licensed under the GNU General Public License v2.
-+# See /LICENSE for more information.
-+
-+START=60
-+
-+USE_PROCD=1
-+
-+lightsd=/usr/bin/lightsd
-+rundir=`$lightsd --rundir`
-+
-+start_service() {
-+	procd_open_instance
-+	procd_set_param command $lightsd -S -v warning -t -u lightsd -s "$rundir/socket" -c "$rundir/pipe"
-+        procd_set_param respawn
-+	procd_close_instance
-+}
-diff --git a/dist/pkgbuild/.SRCINFO b/dist/pkgbuild/.SRCINFO
-new file mode 100644
---- /dev/null
-+++ b/dist/pkgbuild/.SRCINFO
-@@ -0,0 +1,20 @@
-+pkgbase = lightsd
-+	pkgdesc = Daemon to control your LIFX smart bulbs via a JSON-RPC API
-+	pkgver = {{ version }}
-+	pkgrel = {{ build_number }}
-+	epoch = 1
-+	url = https://www.github.com/lopter/lightsd/
-+	install = lightsd.install
-+	arch = i686
-+	arch = x86_64
-+	license = GPL3
-+	makedepends = cmake>=2.8.11
-+	depends = libevent>=2.0.19
-+	optdepends = python: to run the interactive lightsc.py example client
-+	optdepends = ipython: makes lightsc.py more user-friendly
-+	source = {{ archive_url }}
-+	sha256sums = {{ archive_sha256 }}
-+
-+pkgname = lightsd
-+
-+
-diff --git a/dist/pkgbuild/PKGBUILD b/dist/pkgbuild/PKGBUILD
-new file mode 100644
---- /dev/null
-+++ b/dist/pkgbuild/PKGBUILD
-@@ -0,0 +1,43 @@
-+# Maintainer: Louis Opter <kalessin@kalessin.fr>
-+
-+pkgname=lightsd
-+pkgver={{ version }}
-+pkgrel={{ build_number }}
-+epoch=1
-+pkgdesc="Daemon to control your LIFX smart bulbs via a JSON-RPC API"
-+arch=("i686" "x86_64")
-+url="https://www.github.com/lopter/lightsd/"
-+license=("GPL3")
-+depends=("libevent>=2.0.19")
-+optdepends=(
-+    "python: to run the interactive lightsc.py example client"
-+    "ipython: makes lightsc.py more user-friendly"
-+)
-+makedepends=("cmake>=2.8.11")
-+source=("{{ archive_url }}")
-+sha256sums=("{{ archive_sha256 }}")
-+install=lightsd.install
-+
-+build() {
-+    cd "$srcdir/$pkgname-$pkgver"
-+
-+    cmake .                         \
-+        -DCMAKE_BUILD_TYPE=RELEASE  \
-+        -DCMAKE_INSTALL_PREFIX=/usr \
-+        -DLGTD_RUNTIME_DIRECTORY=/run/lightsd
-+
-+    make
-+}
-+
-+check() {
-+    cd "$srcdir/$pkgname-$pkgver"
-+
-+    make test
-+}
-+
-+package() {
-+    cd "$srcdir/$pkgname-$pkgver"
-+
-+    make DESTDIR="$pkgdir/" install
-+}
-+
-diff --git a/dist/pkgbuild/lightsd.install b/dist/pkgbuild/lightsd.install
-new file mode 100644
---- /dev/null
-+++ b/dist/pkgbuild/lightsd.install
-@@ -0,0 +1,22 @@
-+post_install() {
-+    getent group lightsd >/dev/null || groupadd -r lightsd
-+    getent passwd lightsd >/dev/null || useradd -r -d / -g lightsd lightsd
-+
-+    cat << 'EOF'
-+
-+lightsd runs under the `lightsd' user and group by default; add yourself to
-+this group to be able to open lightsd's socket and pipe under /run/lightsd:
-+
-+  gpasswd -a $USER lightsd
-+
-+Re-open your current desktop or ssh session for the change to take effect.
-+Then use systemctl to start lightsd; you can start playing with lightsd with:
-+
-+  `lightsd --prefix`/share/doc/lightsd/examples/lightsc.py
-+
-+EOF
-+}
-+
-+post_remove() {
-+    getent passwd lightsd >/dev/null && userdel lightsd
-+}
-diff --git a/dist/release.py.in b/dist/release.py.in
-new file mode 100644
---- /dev/null
-+++ b/dist/release.py.in
-@@ -0,0 +1,559 @@
-+#!/usr/bin/env python3
-+
-+import click
-+import datetime
-+import functools
-+import hashlib
-+import os
-+import itertools
-+import jinja2
-+import locale
-+import pytz
-+import requests
-+import semver
-+import shutil
-+import subprocess
-+import sys
-+
-+LIGHTSD_VERSION = "@LIGHTSD_VERSION@"
-+# where the lightsd sources are:
-+LIGHTSD_SOURCE_DIR = "@LIGHTSD_SOURCE_DIR@"
-+# where the build is:
-+LIGHTSD_BINARY_DIR = "@LIGHTSD_BINARY_DIR@"
-+# where all the downstream repositories are, they will be pushed by by buildbot
-+# (which manage the credentials to those repositories)
-+LIGHTSD_PKGS_DIR = "@LIGHTSD_RELEASE_PACKAGES_DIR@"
-+# where to put the generated archives served at
-+# https://downloads.lightsd.io/releases/:
-+LIGHTSD_ARCHIVES_DIR = "@LIGHTSD_RELEASE_ARCHIVES_DIR@"
-+# where to put generated debian packages
-+LIGHTSD_DEBS_DIR = "@LIGHTSD_RELEASE_DEBS_DIR@"
-+# where to manage the documentation served at https://docs.lighsd.io/:
-+LIGHTSD_DOCS_DIR = "@LIGHTSD_RELEASE_DOCS_DIR@"
-+
-+# paths to utilities:
-+HG_EXECUTABLE = "@HG_EXECUTABLE@"
-+BSDTAR_EXECUTABLE = "@BSDTAR_EXECUTABLE@"
-+GZIP_EXECUTABLE = "@GZIP_EXECUTABLE@"
-+XZ_EXECUTABLE = "@XZ_EXECUTABLE@"
-+
-+COMMIT_AUTHOR = "Buildbot <buildbot@kalessin.fr>"
-+
-+FILE_READ_SIZE = 32768
-+
-+USER_ENCODING = locale.getpreferredencoding()
-+
-+TARBALL_COMPRESSORS = [
-+    # suffix, path, extra_flags
-+    ("gz", GZIP_EXECUTABLE, ["--best"]),
-+    ("xz", XZ_EXECUTABLE, []),
-+]
-+
-+prereq_echo = functools.partial(click.secho, fg="magenta", bold=True)
-+action_echo = functools.partial(click.secho, fg="blue", bold=True)
-+result_echo = functools.partial(click.secho, fg="green", bold=True)
-+error_echo = functools.partial(click.secho, bold=True)
-+
-+
-+def repopath(repo):
-+    return os.path.join(LIGHTSD_PKGS_DIR, repo)
-+
-+
-+def next_dev_version(version):
-+    parts, version = version, semver.format_version(**version)
-+    if parts["prerelease"] is not None:
-+        return semver.bump_prerelease(version)
-+    return semver.parse(semver.bump_prerelease(semver.bump_patch(version)))
-+
-+
-+def next_pre_release(version):
-+    version = semver.format_version(**version)
-+    return semver.parse(semver.bump_prerelease(version))
-+
-+
-+def next_build_release(version):
-+    version = semver.format_version(**version)
-+    return semver.parse(semver.bump_build(version))
-+
-+
-+def extract_build_number(version):
-+    if version["build"] is None:
-+        return "1"
-+    rv = str(int(version["build"].split(".")[-1]) + 1)
-+    version["build"] = None
-+    return rv
-+
-+
-+def readfile(fp, read_size=FILE_READ_SIZE):
-+    chunk = fp.read(read_size)
-+    while chunk:
-+        yield chunk
-+        chunk = fp.read(read_size)
-+
-+
-+class PackageContext:
-+
-+    __slots__ = (
-+        "version",
-+        "build_number",
-+        "archive_name",
-+        "archive_md5",
-+        "archive_sha256",
-+        "archive_url",
-+    )
-+
-+    def __init__(self, **kwargs):
-+        for name, value in kwargs.items():
-+            if name not in self.__slots__:
-+                raise TypeError("{}: invalid argument {}".format(
-+                    self.__class__.__name__, name
-+                ))
-+            setattr(self, name, value)
-+
-+    def as_dict(self):
-+        return {name: getattr(self, name) for name in self.__slots__}
-+
-+
-+class DownstreamPackage:
-+
-+    TYPE = None
-+    TEMPLATES = None
-+    STATIC_FILES = None
-+
-+    def __init__(self, repo):
-+        self._repo = repo
-+        self._src_dir = os.path.join(LIGHTSD_SOURCE_DIR, "dist", self.TYPE)
-+        jinja_loader = jinja2.FileSystemLoader(self._src_dir)
-+        self._render_ctx = jinja2.Environment(loader=jinja_loader)
-+
-+    def _render(self, msg):
-+        pass
-+
-+    def render(self, pkg_ctx):
-+        if self.TEMPLATES is not None:
-+            for filename in self.TEMPLATES:
-+                template = self._render_ctx.get_template(filename)
-+                with open(os.path.join(self._repo, filename), "wb") as fp:
-+                    template.stream(pkg_ctx.as_dict()).dump(fp, "utf-8")
-+
-+        if self.STATIC_FILES is not None:
-+            for filename in self.STATIC_FILES:
-+                dest = os.path.join(self._src_dir, filename)
-+                dirname = os.path.dirname(dest)
-+                os.makedirs(dirname, exist_ok=True)
-+                shutil.copyfile(dest, os.path.join(self._repo, filename))
-+
-+        self._render(pkg_ctx)
-+
-+    def _pre_commit(self, msg):
-+        pass
-+
-+    def commit(self, msg):
-+        self._pre_commit(msg)
-+
-+        dirty = bool(len(subprocess.check_output([
-+            HG_EXECUTABLE, "-R", self._repo, "status"
-+        ])))
-+        if dirty:
-+            subprocess.check_call([
-+                HG_EXECUTABLE, "-R", self._repo, "commit", "-m", msg
-+            ])
-+        return dirty
-+
-+
-+class DebianPackage(DownstreamPackage):
-+
-+    TYPE = "dpkg"
-+    STATIC_FILES = (
-+        "debian/init.d",
-+        "debian/source/format",
-+        "debian/postinst",
-+        "debian/control",
-+        "debian/postrm",
-+        "debian/copyright",
-+        "debian/rules",
-+        "debian/compat",
-+        "debian/watch",
-+        "debian/docs",
-+        "debian/lightsd.default",
-+    )
-+
-+    _CHANGELOG_ENTRY_FORMAT = (
-+"""lightsd ({version}-{build_number}) unstable; urgency=low
-+
-+  * {msg}
-+
-+ -- {author}  {date}
-+
-+"""  # noqa
-+    )
-+    _CHANGELOG_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S %z"
-+
-+    def _render(self, pkg_ctx):
-+        # change -rc.# into ~rc.# to keep lintian happy
-+        self._version = pkg_ctx.version.replace("-", "~")
-+        self._build_number = pkg_ctx.build_number
-+
-+    def _pre_commit(self, msg):
-+        changelog_path = os.path.join(self._repo, "debian", "changelog")
-+        staging_changelog_path = "-".join([changelog_path, "next"])
-+        user_date_locale = locale.getlocale(locale.LC_TIME)
-+        utcnow = datetime.datetime.now(pytz.utc)
-+
-+        # prepare new changelog:
-+        with open(staging_changelog_path, "wb") as ofp:
-+            try:
-+                locale.setlocale(locale.LC_TIME, "C")
-+                new_entry = self._CHANGELOG_ENTRY_FORMAT.format(
-+                    version=self._version,
-+                    build_number=self._build_number,
-+                    msg=msg,
-+                    author=COMMIT_AUTHOR,
-+                    date=utcnow.strftime(self._CHANGELOG_DATE_FORMAT)
-+                ).encode("utf-8")
-+                ofp.write(new_entry)
-+                with open(changelog_path, "rb") as ifp:
-+                    prev_entry = next(ifp)  # get the first line
-+                    if new_entry.startswith(prev_entry):
-+                        os.unlink(staging_changelog_path)
-+                        return  # we already released that version.
-+                    ofp.write(prev_entry)
-+                    for chunk in readfile(ifp):
-+                        ofp.write(chunk)
-+            except Exception:
-+                os.unlink(staging_changelog_path)
-+                raise
-+            finally:
-+                user_date_locale = locale.setlocale(
-+                    locale.LC_TIME, user_date_locale
-+                )
-+
-+        # and replace the old one:
-+        os.replace(staging_changelog_path, changelog_path)
-+
-+
-+class HomebrewPackage(DownstreamPackage):
-+
-+    TYPE = "homebrew"
-+    TEMPLATES = ("lightsd.rb",)
-+    STATIC_FILES = ("LICENSE", "README.rst")
-+
-+
-+class OpenWRTPackage(DownstreamPackage):
-+
-+    TYPE = "openwrt"
-+    TEMPLATES = ("utils/lightsd/Makefile",)
-+    STATIC_FILES = ("utils/lightsd/files/lightsd.init",)
-+
-+
-+class PKGBUILDPackage(DownstreamPackage):
-+
-+    TYPE = "pkgbuild"
-+    TEMPLATES = ("PKGBUILD", ".SRCINFO")
-+
-+
-+PACKAGES = (
-+    DebianPackage(repopath("dpkg-lightsd")),
-+    HomebrewPackage(repopath("homebrew-lightsd")),
-+    OpenWRTPackage(repopath("openwrt-lightsd")),
-+    PKGBUILDPackage(repopath("pkgbuild-lightsd")),
-+)
-+
-+LIGHTSD_VERSION_CMAKE_TEMPLATE = (
-+"""# NOTE: auto-generated by the release target
-+SET(CPACK_PACKAGE_VERSION_MAJOR "{major}")
-+SET(CPACK_PACKAGE_VERSION_MINOR "{minor}")
-+SET(CPACK_PACKAGE_VERSION_PATCH "{patch}")
-+SET(LIGHTSD_PRERELEASE "{prerelease}")
-+SET(LIGHTSD_BUILD "{build}")
-+SET(LIGHTSD_VERSION "{version}")
-+"""  # noqa
-+)
-+
-+
-+def _abort_if_release_exists(version):
-+    gz_archive_url = (
-+        "https://downloads.lightsd.io/"
-+        "releases/lightsd-{}.tar.gz".format(version)
-+    )
-+
-+    prereq_echo("Checking for an existing release")
-+    response = requests.head(gz_archive_url, allow_redirects=True)
-+    click.echo("[+] {}: {} {}".format(
-+        gz_archive_url, response.status_code, response.reason
-+    ))
-+    if response.status_code != requests.codes.not_found:
-+        error_echo("Release already found at {}!".format(gz_archive_url))
-+        sys.exit(1)
-+
-+
-+def _update_lightsd_version_cmake_include(version):
-+    version_parts = {
-+        k: v if v is not None else ""
-+        for k, v in semver.parse(version).items()
-+    }
-+    lightsd_version_file = os.path.join(
-+        LIGHTSD_SOURCE_DIR, "CMakeScripts", "LightsdVersion.cmake"
-+    )
-+    with open(lightsd_version_file, "wb") as fp:
-+        fp.write(LIGHTSD_VERSION_CMAKE_TEMPLATE.format(
-+            version=version, **version_parts
-+        ).encode("utf-8"))
-+
-+
-+def _release(version, next_version):
-+    revision, branch = [
-+        part.strip() for part in subprocess.check_output([
-+            HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR, "id", "--id", "--branch"
-+        ]).decode(USER_ENCODING).split()
-+    ]
-+    qapplied = bool(len(subprocess.check_output([
-+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
-+        "--config", "extensions.mq=", "qapplied"
-+    ]).decode(USER_ENCODING)))
-+    if qapplied or revision.endswith("+") or branch != "default":
-+        error_echo(
-+            "Can't do a release over a dirty repository! "
-+            "(rev={}, branch={}, patched={})".format(
-+                revision, branch, qapplied
-+            )
-+        )
-+        sys.exit(1)
-+
-+    # Re-gen LightsdVersion.cmake with major.minor.patch, tag, and re-gen
-+    # LightsdVersion.cmake after bumping minor adding a prerelease tag:
-+    version = click.prompt(
-+        "Confirm the version to release",
-+        default=version,
-+        type=lambda arg: semver.format_version(**semver.parse(arg))
-+    )
-+    _update_lightsd_version_cmake_include(version)
-+    subprocess.check_call([
-+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
-+        "tag", "-m", "Tagging release {}".format(version), version
-+        # TODO: Use docutils to extract the changelog section
-+    ])
-+    _update_lightsd_version_cmake_include(next_version)
-+    subprocess.check_call([
-+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
-+        "commit", "-m", "Back to development, {}".format(next_version)
-+    ])
-+
-+    subprocess.check_call([HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR, "out"])
-+    if click.confirm("Are you ready to push those commit?"):
-+        return
-+        subprocess.check_call([HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR, "push"])
-+
-+
-+@click.group()
-+def cli():
-+    pass
-+
-+
-+@cli.command()
-+def pre_release():
-+    _abort_if_release_exists(LIGHTSD_VERSION)
-+    version = semver.parse(LIGHTSD_VERSION)
-+    if version["prerelease"] is None:
-+        version = next_pre_release(version)
-+    _release(
-+        semver.format_version(**version),
-+        semver.format_version(**next_pre_release(version)),
-+    )
-+
-+
-+@cli.command()
-+def release():
-+    _abort_if_release_exists(LIGHTSD_VERSION)
-+    version = semver.parse(LIGHTSD_VERSION)
-+    version["prerelease"] = version["build"] = None
-+    _release(
-+        semver.format_version(**version),
-+        semver.format_version(**next_dev_version(version)),
-+    )
-+
-+
-+@cli.command()
-+def package_release(release):
-+    version = semver.parse(LIGHTSD_VERSION)
-+    # TODO: version = previous_release (including build metadata)
-+    #       (maybe downloads.lightsd.io can help)
-+    version = next_build_release(version)
-+    _release(
-+        semver.format_version(**version),
-+        semver.format_version(**next_build_release(version)),
-+    )
-+
-+
-+@cli.command()
-+def release_new_tag():
-+    if not all([LIGHTSD_PKGS_DIR, LIGHTSD_ARCHIVES_DIR]):
-+        error_echo(
-+            "Please configure the project with LIGHTSD_RELEASE_PACKAGES_DIR "
-+            "and LIGHTSD_RELEASE_ARCHIVES_DIR to use this command."
-+        )
-+        sys.exit(1)
-+
-+    _abort_if_release_exists(LIGHTSD_VERSION)
-+
-+    prereq_echo("Cleaning-up the source tree")
-+    subprocess.check_call([
-+        HG_EXECUTABLE, "-R", LIGHTSD_SOURCE_DIR,
-+        "--config", "extensions.purge=", "purge", "--abort-on-err", "--all"
-+    ])
-+
-+    archive_name = "lightsd-{}.tar".format(LIGHTSD_VERSION)
-+    archive = os.path.join(LIGHTSD_BINARY_DIR, "dist", archive_name)
-+    gz_archive_name = ".".join([archive_name, "gz"])
-+    gz_archive = os.path.join(LIGHTSD_ARCHIVES_DIR, gz_archive_name)
-+    gz_archive_url = "https://downloads.lightsd.io/releases/{}".format(
-+        gz_archive_name
-+    )
-+
-+    # NOTE: I wanted to use hg archive but then dpkg gave me troubles because
-+    #       the archive had extra files or something:
-+    action_echo("Tarballing the sources into {}".format(archive))
-+    if not os.path.exists(archive):
-+        subprocess.check_call([
-+            BSDTAR_EXECUTABLE,
-+            "-C", LIGHTSD_SOURCE_DIR,
-+            "-cf", archive,
-+            # Put everything under a top-level directory ("archive_name"
-+            # without the .tar extension):
-+            "-s", "/^\\./{}/".format(archive_name.rsplit(".", 1)[0]),
-+            "--exclude", ".hg",
-+            "."
-+        ])
-+    os.makedirs(LIGHTSD_ARCHIVES_DIR, exist_ok=True)
-+    for suffix, bin, extra_flags in TARBALL_COMPRESSORS:
-+        dest = ".".join([
-+            os.path.join(LIGHTSD_ARCHIVES_DIR, archive_name), suffix
-+        ])
-+        if os.path.exists(dest):
-+            click.echo("[=] compressing with {}".format(suffix))
-+            continue
-+        cmd = itertools.chain([bin, "--stdout"], extra_flags, [archive])
-+        click.echo("[+] compressing with {}".format(suffix))
-+        with open(dest, "wb") as fp:
-+            subprocess.check_call(cmd, stdout=fp)
-+
-+    action_echo("Computing MD5 and SHA256 checksums for {}".format(gz_archive))
-+    gz_archive_md5 = hashlib.md5()
-+    gz_archive_sha256 = hashlib.sha256()
-+    with open(gz_archive, "rb") as fp:
-+        for chunk in readfile(fp):
-+            gz_archive_md5.update(chunk)
-+            gz_archive_sha256.update(chunk)
-+    gz_archive_md5 = gz_archive_md5.hexdigest()
-+    gz_archive_sha256 = gz_archive_sha256.hexdigest()
-+    click.echo("[+] MD5 {}".format(gz_archive_md5))
-+    click.echo("[+] SHA256 {}".format(gz_archive_sha256))
-+
-+    version = semver.parse(LIGHTSD_VERSION)
-+    # NOTE: It would be cool to know which package really changed so we can just
-+    #       render and release that one. I guess the easiest way is to implement
-+    #       that with different schedulers in buildbot or a scheduler that's
-+    #       able to fill-in a property that could be passed to CMake (even
-+    #       better: generate a release target for each pkg).
-+    build_number = extract_build_number(version)
-+    pkg_ctx = PackageContext(
-+        version=semver.format_version(**version),
-+        build_number=build_number,
-+        archive_name=gz_archive_name,
-+        archive_md5=gz_archive_md5,
-+        archive_sha256=gz_archive_sha256,
-+        archive_url=gz_archive_url,
-+    )
-+    action_echo("Updating packages")
-+    release_type = "upstream" if build_number == "1" else "package"
-+    release_msg = "New {} release {}".format(release_type, LIGHTSD_VERSION)
-+    for pkg in PACKAGES:
-+        pkg.render(pkg_ctx)
-+        dirty = pkg.commit(release_msg)
-+        if not dirty:
-+            click.echo("[=] {}".format(pkg.TYPE))
-+            continue
-+        click.echo("[+] {} package".format(pkg.TYPE))
-+
-+    result_echo(release_msg.replace("upstream", "downstream"))
-+
-+
-+@cli.command()
-+def release_docs():
-+    version = semver.parse(LIGHTSD_VERSION)
-+    dest_dir = os.path.join(LIGHTSD_DOCS_DIR, LIGHTSD_VERSION)
-+    docs_dir = os.path.join(LIGHTSD_BINARY_DIR, "docs", "_build", ".")
-+    os.makedirs(dest_dir, exist_ok=True)
-+    if version["prerelease"] is None and version["build"] is None:
-+        alias = "current"
-+    else:
-+        alias = "latest"
-+    action_echo("Copying files into {}".format(dest_dir))
-+    subprocess.check_call(["cp", "-av", docs_dir, dest_dir])
-+    action_echo("Updating alias {}".format(alias))
-+    subprocess.check_call([
-+        "ln", "-vrsnf", dest_dir, os.path.join(LIGHTSD_DOCS_DIR, alias)
-+    ])
-+    result_echo("{} ({}) updated".format(dest_dir, alias))
-+
-+
-+@cli.command()
-+def release_debuild():
-+    if not all([LIGHTSD_DEBS_DIR, LIGHTSD_ARCHIVES_DIR]):
-+        error_echo(
-+            "Please configure the project with LIGHTSD_RELEASE_DEBS_DIR "
-+            "and LIGHTSD_RELEASE_ARCHIVES_DIR to use this command."
-+        )
-+        sys.exit(1)
-+
-+    # This is just too painful to do from buildbot atm (we need to parametrize
-+    # the build with the version):
-+    debuild_dir = os.path.join(LIGHTSD_BINARY_DIR, "dist", "debuild")
-+    if os.path.exists(debuild_dir):
-+        prereq_echo("Cleaning-up previous build in {}".format(debuild_dir))
-+        shutil.rmtree(debuild_dir)
-+    os.makedirs(debuild_dir, exist_ok=True)
-+    version = semver.parse(LIGHTSD_VERSION)
-+    build_number = extract_build_number(version)
-+    version = semver.format_version(**version)
-+    archive_name = "lightsd-{}.tar.gz".format(version)
-+    archive_path = os.path.join(LIGHTSD_ARCHIVES_DIR, archive_name)
-+    deb_archive_name = archive_name.replace("-rc", "~rc")
-+    deb_archive_name = deb_archive_name.replace("lightsd-", "lightsd_")
-+    prereq_echo("Setting-up sources under {}".format(debuild_dir))
-+    deb_archive_path = os.path.join(debuild_dir, deb_archive_name)
-+    shutil.copyfile(archive_path, deb_archive_path)
-+    click.echo("[+] Archive copied to {}".format(deb_archive_path))
-+    src_dir = os.path.join(debuild_dir, "lightsd-{}".format(version))
-+    subprocess.check_call([
-+        BSDTAR_EXECUTABLE, "-C", debuild_dir, "-xzf", deb_archive_path,
-+    ])
-+    click.echo("[+] Archive extracted in {}".format(src_dir))
-+    shutil.copytree(
-+        os.path.join(LIGHTSD_PKGS_DIR, "dpkg-lightsd", "debian"),
-+        os.path.join(src_dir, "debian")
-+    )
-+    click.echo("[+] dpkg sources copied to {}".format(
-+        os.path.join(src_dir, "debian")
-+    ))
-+    dpkg_arch = subprocess.check_output(["dpkg", "--print-architecture"])
-+    dpkg_arch = dpkg_arch.decode(USER_ENCODING)
-+    deb_pkg_name = "lightsd_{version}-{build_number}_{dpkg_arch}.deb".format(
-+        version=version,
-+        build_number=build_number,
-+        dpkg_arch=dpkg_arch,
-+    )
-+    action_echo("Building {}".format(deb_pkg_name))
-+    subprocess.check_call(["debuild"], cwd=src_dir)
-+    shutil.copyfile(
-+        os.path.join(debuild_dir, deb_pkg_name),
-+        os.path.join(LIGHTSD_DEBS_DIR, deb_pkg_name),
-+    )
-+    click.echo("[+] Copied {} under {}".format(
-+        deb_pkg_name, LIGHTSD_DEBS_DIR
-+    ))
-+    result_echo("New Debian package {}".format(deb_pkg_name))
-+
-+
-+if __name__ == "__main__":
-+    cli()
-diff --git a/dist/requirements-release.txt b/dist/requirements-release.txt
-new file mode 100644
---- /dev/null
-+++ b/dist/requirements-release.txt
-@@ -0,0 +1,6 @@
-+click>=6.6,<7.0
-+jinja2>=2.8,<3.0
-+requests>=2.9.1,<3.0
-+semver>=2.4.1,<3.0.0
-+
-+pytz
--- a/docs_setup_alabaster_and_ga.patch	Sat May 28 18:42:04 2016 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-# HG changeset patch
-# Parent  0997d02e7da2e52f7d1dba3783a6fee58fc730eb
-Setup the alabaster theme and Google Analytics for the docs
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -115,6 +115,9 @@
- 
- IF (SPHINX_FOUND)
-     MESSAGE(STATUS "Sphinx found, docs generation enabled")
-+    IF (DEFINED LIGHTSD_GOOGLE_ANALYTICS_ID)
-+      MESSAGE(STATUS "Using Google Analytics ID ${LIGHTSD_GOOGLE_ANALYTICS_ID} for the docs")
-+    ENDIF ()
-     ADD_SUBDIRECTORY(docs)
- ELSE ()
-     MESSAGE(STATUS "Shpinx wasn't found, docs generation disabled")
-diff --git a/docs/conf.py.in b/docs/conf.py.in
---- a/docs/conf.py.in
-+++ b/docs/conf.py.in
-@@ -13,7 +13,8 @@
- # All configuration values have a default; values that are commented out
- # serve to show the default.
- 
--import sys
-+from __future__ import unicode_literals
-+
- import os
- 
- # If extensions (or modules to document with autodoc) are in another directory,
-@@ -34,7 +35,7 @@
- extlinks = {'gh': ('https://github.com/lopter/lightsd/issues/%s', 'GH-')}
- 
- # Add any paths that contain templates here, relative to this directory.
--templates_path = ['_templates']
-+templates_path = [os.path.join('@CMAKE_CURRENT_SOURCE_DIR@', '_templates')]
- 
- # The suffix of source filenames.
- source_suffix = '.rst'
-@@ -47,14 +48,14 @@
- 
- # General information about the project.
- project = 'lightsd'
--copyright = '2015, Louis Opter'
-+copyright = '2015 - 2016, <a href="https://www.kalessin.fr/">Louis Opter</a>'
- 
- # The version info for the project you're documenting, acts as replacement for
- # |version| and |release|, also used in various other places throughout the
- # built documents.
- #
- # The short X.Y version.
--version = '@LIGHTSD_VERSION@'
-+version = '@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@'
- # The full version, including alpha/beta/rc tags.
- release = '@LIGHTSD_VERSION@'
- 
-@@ -101,12 +102,18 @@
- 
- # The theme to use for HTML and HTML Help pages.  See the documentation for
- # a list of builtin themes.
--html_theme = 'nature'
-+html_theme = 'alabaster'
- 
- # Theme options are theme-specific and customize the look and feel of a theme
- # further.  For a list of options available for each theme, see the
- # documentation.
--#html_theme_options = {}
-+html_theme_options = {
-+    "github_user": "lopter",
-+    "github_repo": "lightsd",
-+    "github_button": True,
-+    "analytics_id": "@LIGHTSD_GOOGLE_ANALYTICS_ID@",
-+    "show_powered_by": False,
-+}
- 
- # Add any paths that contain custom themes here, relative to this directory.
- #html_theme_path = []
-@@ -146,7 +153,15 @@
- #html_use_smartypants = True
- 
- # Custom sidebar templates, maps document names to template names.
--#html_sidebars = {}
-+html_sidebars = {
-+    '**': [
-+        'about.html',
-+        'navigation.html',
-+        'relations.html',
-+        'searchbox.html',
-+        'donate.html',
-+    ]
-+}
- 
- # Additional templates that should be rendered to pages, maps page names to
- # template names.
-@@ -162,7 +177,7 @@
- #html_split_index = False
- 
- # If true, links to the reST sources are added to the pages.
--#html_show_sourcelink = True
-+html_show_sourcelink = False
- 
- # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
- #html_show_sphinx = True
--- a/dont_use_ev_assign.patch	Sat May 28 18:42:04 2016 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-# HG changeset patch
-# Parent  eaec17795512845ed7a35e486ee5e9d61aa429a2
-Avoid event_assign for better compatibility across libevent versions
-
-diff --git a/core/lightsd.c b/core/lightsd.c
---- a/core/lightsd.c
-+++ b/core/lightsd.c
-@@ -36,7 +36,6 @@
- #include <unistd.h>
- 
- #include <event2/event.h>
--#include <event2/event_struct.h>
- 
- #include "lifx/wire_proto.h"
- #include "time_monotonic.h"
-@@ -72,36 +71,22 @@
- 
- struct event_base *lgtd_ev_base = NULL;
- 
-+static const int lgtd_signals[] = { SIGINT, SIGTERM, SIGQUIT };
-+static struct event *lgtd_signal_evs[LGTD_ARRAY_SIZE(lgtd_signals)] = { NULL };
-+
- static int lgtd_last_signal_received = 0;
- 
--void
--lgtd_cleanup(void)
--{
--    lgtd_lifx_discovery_close();
--    lgtd_listen_close_all();
--    lgtd_command_pipe_close_all();
--    lgtd_client_close_all();
--    lgtd_lifx_broadcast_close();
--    lgtd_lifx_gateway_close_all();
--    lgtd_timer_stop_all();
--    event_base_free(lgtd_ev_base);
--#if LIBEVENT_VERSION_NUMBER >= 0x02010100
--    libevent_global_shutdown();
--#endif
--    if (lgtd_opts.pidfile) {
--        unlink(lgtd_opts.pidfile);
--    }
--}
--
- static void
- lgtd_signal_event_callback(int signum, short events, void *ctx)
- {
--    assert(ctx);
--
--    // NOTE: syslog isn't signal safe, don't log anything in this function.
-+    int i = (int)ctx;
-+    assert(i >= 0);
-+    assert(i < (int)LGTD_ARRAY_SIZE(lgtd_signals));
-+    assert(i < (int)LGTD_ARRAY_SIZE(lgtd_signal_evs));
-+    assert(signum == lgtd_signals[i]);
- 
-     lgtd_last_signal_received = signum;
--    event_del((struct event *)ctx);  // restore default behavior
-+    event_del(lgtd_signal_evs[i]);  // restore default behavior
-     event_base_loopbreak(lgtd_ev_base);
-     (void)events;
- }
-@@ -126,20 +111,21 @@
- }
- 
- static void
--lgtd_configure_signal_handling(void)
-+lgtd_setup_signal_handling(void)
- {
--    const int signals[] = {SIGINT, SIGTERM, SIGQUIT};
--    static struct event sigevs[LGTD_ARRAY_SIZE(signals)];
--
--    for (int i = 0; i != LGTD_ARRAY_SIZE(signals); i++) {
--        evsignal_assign(
--            &sigevs[i],
-+    for (int i = 0; i != LGTD_ARRAY_SIZE(lgtd_signals); i++) {
-+        lgtd_signal_evs[i] = evsignal_new(
-             lgtd_ev_base,
--            signals[i],
-+            lgtd_signals[i],
-             lgtd_signal_event_callback,
--            &sigevs[i]
-+            // event_self_cbarg() would make things cleaner, but this was
-+            // unfortunately added in libevent 2.1 which hasn't been released
-+            // as of 2016:
-+            (void *)(intptr_t)i // cast twice for -Wint-to-void-pointer-cast
-         );
--        evsignal_add(&sigevs[i], NULL);
-+        if (!lgtd_signal_evs[i] || evsignal_add(lgtd_signal_evs[i], NULL)) {
-+            lgtd_err(1, "can't configure signal handling");
-+        }
-     }
- 
-     struct sigaction act = { .sa_handler = SIG_IGN };
-@@ -149,6 +135,15 @@
- }
- 
- static void
-+lgtd_close_signal_handling(void)
-+{
-+    for (int i = 0; i != LGTD_ARRAY_SIZE(lgtd_signals); i++) {
-+        event_del(lgtd_signal_evs[i]);
-+        event_free(lgtd_signal_evs[i]);
-+    }
-+}
-+
-+static void
- lgtd_usage(const char *progname)
- {
-     printf(
-@@ -186,6 +181,26 @@
-     exit(0);
- }
- 
-+void
-+lgtd_cleanup(void)
-+{
-+    lgtd_lifx_discovery_close();
-+    lgtd_listen_close_all();
-+    lgtd_command_pipe_close_all();
-+    lgtd_client_close_all();
-+    lgtd_lifx_broadcast_close();
-+    lgtd_lifx_gateway_close_all();
-+    lgtd_timer_stop_all();
-+    lgtd_close_signal_handling();
-+    event_base_free(lgtd_ev_base);
-+#if LIBEVENT_VERSION_NUMBER >= 0x02010100
-+    libevent_global_shutdown();
-+#endif
-+    if (lgtd_opts.pidfile) {
-+        unlink(lgtd_opts.pidfile);
-+    }
-+}
-+
- int
- main(int argc, char *argv[], char *envp[])
- {
-@@ -195,7 +210,7 @@
-     lgtd_daemon_setup_proctitle(argc, argv, envp);
- 
-     lgtd_configure_libevent();
--    lgtd_configure_signal_handling();
-+    lgtd_setup_signal_handling();
- 
-     static const struct option long_opts[] = {
-         {"listen",          required_argument, NULL, 'l'},
--- a/fix_freebsd_build.patch	Sat May 28 18:42:04 2016 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-# HG changeset patch
-# Parent  28701ca70422b882028b7fd0e6c52401b394041a
-Fix build on FreeBSD 10.3
-
-- properly use _XOPEN_SOURCE=700 instead of _POSIX_C_SOURCE;
-- fix missing includes;
-- add compat file for endian.h (maybe it should be the other way or
-  maybe I should just improve the CMake "finder").
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -55,14 +55,20 @@
- SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wextra -Wall -Wstrict-prototypes -std=c99")
- SET(CMAKE_C_FLAGS_${CMAKE_BUILD_TYPE} "")
- 
-+IF (CMAKE_SYSTEM_NAME MATCHES "Linux")
-+    ADD_DEFINITIONS(
-+        # see feature_test_macros(7)
-+        "-D_XOPEN_SOURCE=700"
-+        "-D_BSD_SOURCE=1"
-+        "-D_DEFAULT_SOURCE=1"
-+    )
-+ENDIF ()
-+
-+IF (APPLE)
-+    ADD_DEFINITIONS("-D_DARWIN_C_SOURCE=1")
-+ENDIF ()
-+
- ADD_DEFINITIONS(
--    # Only relevant for the GNU libc:
--    "-D_POSIX_C_SOURCE=200809L"
--    "-D_BSD_SOURCE=1"
--    "-D_DEFAULT_SOURCE=1"
--
--    "-D_DARWIN_C_SOURCE=1"
--
-     "-DLGTD_BIG_ENDIAN_SYSTEM=${BIG_ENDIAN_SYSTEM}"
-     "-DLGTD_SIZEOF_VOID_P=${CMAKE_SIZEOF_VOID_P}"
- 
-diff --git a/compat/FreeBSD/endian.h b/compat/FreeBSD/endian.h
-new file mode 100644
---- /dev/null
-+++ b/compat/FreeBSD/endian.h
-@@ -0,0 +1,3 @@
-+#pragma once
-+
-+#include <sys/endian.h>
-diff --git a/core/listen.c b/core/listen.c
---- a/core/listen.c
-+++ b/core/listen.c
-@@ -19,6 +19,7 @@
- #include <sys/socket.h>
- #include <sys/stat.h>
- #include <sys/un.h>
-+#include <netinet/in.h>
- #include <assert.h>
- #include <err.h>
- #include <errno.h>
-diff --git a/lifx/broadcast.c b/lifx/broadcast.c
---- a/lifx/broadcast.c
-+++ b/lifx/broadcast.c
-@@ -18,6 +18,7 @@
- #include <sys/queue.h>
- #include <sys/tree.h>
- #include <arpa/inet.h>
-+#include <netinet/in.h>
- #include <assert.h>
- #include <endian.h>
- #include <err.h>
-diff --git a/lifx/gateway.c b/lifx/gateway.c
---- a/lifx/gateway.c
-+++ b/lifx/gateway.c
-@@ -17,6 +17,7 @@
- 
- #include <sys/queue.h>
- #include <sys/tree.h>
-+#include <netinet/in.h>
- #include <assert.h>
- #include <endian.h>
- #include <err.h>
-diff --git a/tests/core/pipe/test_pipe_open_fifo_already_exists.c b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
---- a/tests/core/pipe/test_pipe_open_fifo_already_exists.c
-+++ b/tests/core/pipe/test_pipe_open_fifo_already_exists.c
-@@ -1,6 +1,7 @@
- #include "pipe.c"
- 
- #include <sys/tree.h>
-+#include <sys/stat.h>
- #include <endian.h>
- #include <limits.h>
- 
--- a/network_discovery.patch	Sat May 28 18:42:04 2016 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1322 +0,0 @@
-# HG changeset patch
-# Parent  3b2d568c18f44ef51ec69c4b8d19eb15e8b7a057
-Properly broadcast LIFX discovery packets on all networks (closes GH-2)
-
-This is really the proper way of achieving the same semantic as
-broadcasting to 255.255.255.255 and is kinda half the solution only.
-What I'd like to add is:
-
-- bind to all local-only networks (right now lightsd binds on 0.0.0.0);
-- watch for network interfaces changes so we can re-bind if necessary,
-  this is really the hard and annoying part as each OS has its own
-  mechanism for this and some (e.g: BSDs) don't even have it AFAIK.
-
-So that people running lightsd on a machine connected to Internet (that
-can happen quickly on a laptop or a phone/tabled) don't have to firewall
-56700.
-
-diff --git a/core/lightsd.h b/core/lightsd.h
---- a/core/lightsd.h
-+++ b/core/lightsd.h
-@@ -41,7 +41,9 @@
- #define LGTD_ABS(v) ((v) >= 0 ? (v) : (v) * -1)
- #define LGTD_MIN(a, b) ((a) < (b) ? (a) : (b))
- #define LGTD_MAX(a, b) ((a) > (b) ? (a) : (b))
-+
- #define LGTD_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
-+
- #define LGTD_MSECS_TO_TIMEVAL(v) {  \
-     .tv_sec = (v) / 1000,           \
-     .tv_usec = ((v) % 1000) * 1000  \
-@@ -49,6 +51,7 @@
- #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)) {                                                               \
-@@ -69,11 +72,24 @@
-         );                                                                      \
-     }                                                                           \
- } 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);                         \
- } while (0)
- 
-+#if LGTD_BIG_ENDIAN_SYSTEM
-+# define LGTD_STATIC_HTONS(s) (s)
-+# define LGTD_STATIC_HTONL(l) (l)
-+#else
-+# define LGTD_STATIC_HTONS(s) ((((s) << 8) & 0xff00) | (((s) >> 8) & 0xff))
-+# define LGTD_STATIC_HTONL(l) (                                 \
-+    (((l) & 0xff000000) >> 24) | (((l) & 0x00ff0000) >> 8)      \
-+    | (((l) & 0x0000ff00) << 8) | (((l) & 0x000000ff) << 24)    \
-+)
-+#endif
-+
-+
- enum lgtd_verbosity {
-     LGTD_DEBUG = 0,
-     LGTD_INFO,
-diff --git a/lifx/broadcast.c b/lifx/broadcast.c
---- a/lifx/broadcast.c
-+++ b/lifx/broadcast.c
-@@ -17,12 +17,15 @@
- 
- #include <sys/queue.h>
- #include <sys/tree.h>
-+#include <sys/socket.h>
- #include <arpa/inet.h>
-+#include <net/if.h>
- #include <netinet/in.h>
- #include <assert.h>
- #include <endian.h>
- #include <err.h>
- #include <errno.h>
-+#include <ifaddrs.h>
- #include <stdarg.h>
- #include <stdbool.h>
- #include <stdint.h>
-@@ -52,16 +55,37 @@
- };
- 
- static bool
-+lgtd_lifx_broadcast_send_packet(const void *pkt,
-+                                int pkt_sz,
-+                                const struct sockaddr *addr,
-+                                ev_socklen_t addrlen)
-+{
-+    char addr_str[INET6_ADDRSTRLEN];
-+    LGTD_SOCKADDRTOA(addr, addr_str);
-+    lgtd_debug("broadcasting LIFX discovery packet on %s", addr_str);
-+
-+    int nbytes, socket = lgtd_lifx_broadcast_endpoint.socket;
-+    do {
-+        nbytes = sendto(socket, pkt, pkt_sz, 0, addr, addrlen);
-+    } while (nbytes == -1 && EVUTIL_SOCKET_ERROR() == EINTR);
-+
-+    if (nbytes != pkt_sz) {
-+        void (*warnfn)(const char *fmt, ...) = nbytes == -1 ?
-+            lgtd_warn : lgtd_warnx;
-+        warnfn("couldn't broadcast LIFX discovery packet on %s", addr_str);
-+        return false;
-+    }
-+
-+    return true;
-+}
-+
-+static bool
- lgtd_lifx_broadcast_handle_write(void)
- {
-     assert(lgtd_lifx_broadcast_endpoint.socket != -1);
- 
--    struct sockaddr_in lifx_addr = {
--        .sin_family = AF_INET,
--        .sin_addr = { INADDR_BROADCAST },
--        .sin_port = htons(LGTD_LIFX_PROTOCOL_PORT),
--        .sin_zero = { 0 }
--    };
-+    const uint16_t lifx_port = htons(LGTD_LIFX_PROTOCOL_PORT);
-+
-     struct lgtd_lifx_packet_header get_pan_gateway;
-     lgtd_lifx_wire_setup_header(
-         &get_pan_gateway,
-@@ -71,31 +95,50 @@
-         LGTD_LIFX_GET_PAN_GATEWAY
-     );
- 
--    int nbytes;
--retry:
--    nbytes = sendto(
--        lgtd_lifx_broadcast_endpoint.socket,
--        (void *)&get_pan_gateway,
--        sizeof(get_pan_gateway),
--        0,
--        (const struct sockaddr *)&lifx_addr,
--        sizeof(lifx_addr)
--    );
--    if (nbytes == sizeof(get_pan_gateway)) {
--        if (event_del(lgtd_lifx_broadcast_endpoint.write_ev)) {
--            lgtd_err(1, "can't setup events");
-+    bool ok = false;
-+    struct ifaddrs *ifaddrs = NULL;
-+    if (getifaddrs(&ifaddrs)) {
-+        struct sockaddr_in lifx_bcast_addr = {
-+            .sin_family = AF_INET,
-+            .sin_addr = { INADDR_BROADCAST },
-+            .sin_port = lifx_port,
-+            .sin_zero = { 0 }
-+        };
-+        char addr_str[INET6_ADDRSTRLEN];
-+        lgtd_warn(
-+            "can't fetch the list of network interfaces, falling back on %s",
-+            LGTD_SOCKADDRTOA((struct sockaddr *)&lifx_bcast_addr, addr_str)
-+        );
-+        ok = lgtd_lifx_broadcast_send_packet(
-+            &get_pan_gateway,
-+            sizeof(get_pan_gateway),
-+            (struct sockaddr *)&lifx_bcast_addr,
-+            sizeof(lifx_bcast_addr)
-+        );
-+    } else {
-+        for (struct ifaddrs *ifa = ifaddrs; ifa; ifa = ifa->ifa_next) {
-+            // NOTE: IPv6 doesn't implement broadcast
-+            if (ifa->ifa_broadaddr != NULL
-+                && (ifa->ifa_flags & IFF_BROADCAST)
-+                && ifa->ifa_broadaddr->sa_family == AF_INET
-+                && ifa->ifa_netmask != NULL) {
-+                struct sockaddr *addr = ifa->ifa_broadaddr;
-+                ((struct sockaddr_in *)addr)->sin_port = lifx_port;
-+                ev_socklen_t addrlen = sizeof(struct sockaddr_in);
-+                bool sent = lgtd_lifx_broadcast_send_packet(
-+                    &get_pan_gateway, sizeof(get_pan_gateway), addr, addrlen
-+                );
-+                ok = sent || ok;
-+            }
-         }
--        return true;
-+        freeifaddrs(ifaddrs);
-     }
--    if (nbytes == -1) {
--        if (EVUTIL_SOCKET_ERROR() == EINTR) {
--            goto retry;
--        }
--        lgtd_warn("can't broadcast discovery packet");
--    } else {
--        lgtd_warnx("can't broadcast discovery packet");
-+
-+    if (ok && event_del(lgtd_lifx_broadcast_endpoint.write_ev)) {
-+        lgtd_err(1, "can't setup events");
-     }
--    return false;
-+
-+    return ok;
- }
- 
- static void
-diff --git a/lifx/bulb.c b/lifx/bulb.c
---- a/lifx/bulb.c
-+++ b/lifx/bulb.c
-@@ -274,9 +274,9 @@
- 
- 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)
-+                            enum lgtd_lifx_bulb_ips ip_id,
-+                            const struct lgtd_lifx_ip_state *state,
-+                            lgtd_time_mono_t received_at)
- {
-     assert(bulb);
-     assert(state);
-diff --git a/lifx/tagging.h b/lifx/tagging.h
---- a/lifx/tagging.h
-+++ b/lifx/tagging.h
-@@ -17,6 +17,8 @@
- 
- #pragma once
- 
-+struct lgtd_lifx_gateway;
-+
- extern struct lgtd_lifx_tag_list lgtd_lifx_tags;
- 
- struct lgtd_lifx_site {
-diff --git a/tests/core/mock_event2.h b/tests/core/mock_event2.h
---- a/tests/core/mock_event2.h
-+++ b/tests/core/mock_event2.h
-@@ -210,3 +210,21 @@
-     return 0;
- }
- #endif
-+
-+#ifndef MOCKED_EVUTIL_CLOSESOCKET
-+int
-+evutil_closesocket(evutil_socket_t sock)
-+{
-+    (void)sock;
-+    return -1;
-+}
-+#endif
-+
-+#ifndef MOCKED_EVUTIL_MAKE_LISTEN_SOCKET_REUSEABLE
-+int
-+evutil_make_listen_socket_reuseable(evutil_socket_t sock)
-+{
-+    (void)sock;
-+    return -1;
-+}
-+#endif
-diff --git a/tests/core/tests_utils.c b/tests/core/tests_utils.c
---- a/tests/core/tests_utils.c
-+++ b/tests/core/tests_utils.c
-@@ -213,3 +213,39 @@
- 
-     free(path);
- }
-+
-+void
-+lgtd_tests_check_sockaddr_in(const struct sockaddr *addr,
-+                             int addrlen,
-+                             int expected_family,
-+                             uint32_t expected_addr,
-+                             uint16_t expected_port)
-+{
-+    if (!addr) {
-+        lgtd_errx(1, "missing addr");
-+    }
-+
-+    if (addr->sa_family != expected_family) {
-+        lgtd_errx(
-+            1, "got address of type %d (expected %d)",
-+            addr->sa_family, expected_family
-+        );
-+    }
-+    const struct sockaddr_in *inaddr = (const struct sockaddr_in *)addr;
-+    if (inaddr->sin_addr.s_addr != expected_addr) {
-+        lgtd_errx(
-+            1, "got address %#x (expected %#x)",
-+            inaddr->sin_addr.s_addr, expected_addr
-+        );
-+    }
-+    uint16_t port = ntohs(inaddr->sin_port);
-+    if (port != expected_port) {
-+        lgtd_errx(1, "got port %hu (expected %u)", port, expected_port);
-+    }
-+    if (addrlen != -1 && addrlen != sizeof(*inaddr)) {
-+        lgtd_errx(
-+            1, "got invalid addrlen %u (expected %ju)",
-+            addrlen, (uintmax_t)sizeof(*inaddr)
-+        );
-+    }
-+}
-diff --git a/tests/core/tests_utils.h b/tests/core/tests_utils.h
---- a/tests/core/tests_utils.h
-+++ b/tests/core/tests_utils.h
-@@ -1,6 +1,7 @@
- #pragma once
- 
- struct bufferevent;
-+struct sockaddr;
- 
- static inline bool
- lgtd_tests_lifx_header_has_flags(const struct lgtd_lifx_packet_header *hdr,
-@@ -32,6 +33,12 @@
-     return true;
- }
- 
-+static inline void
-+lgtd_tests_hr(void)
-+{
-+    puts("----");
-+}
-+
- char *lgtd_tests_make_temp_dir(void);
- void lgtd_tests_remove_temp_dir(char *);
- 
-@@ -44,3 +51,4 @@
-                                                 int);
- struct lgtd_listen *lgtd_tests_insert_mock_listener(const char *, uint16_t);
- struct lgtd_client *lgtd_tests_insert_mock_client(struct bufferevent *);
-+void lgtd_tests_check_sockaddr_in(const struct sockaddr *, int, int, uint32_t, uint16_t);
-diff --git a/tests/lifx/broadcast/CMakeLists.txt b/tests/lifx/broadcast/CMakeLists.txt
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/broadcast/CMakeLists.txt
-@@ -0,0 +1,21 @@
-+INCLUDE_DIRECTORIES(
-+    ${CMAKE_CURRENT_SOURCE_DIR}
-+    ${CMAKE_CURRENT_BINARY_DIR}
-+)
-+
-+ADD_CORE_LIBRARY(
-+    test_lifx_broadcast STATIC
-+    ${LIGHTSD_SOURCE_DIR}/core/stats.c
-+    ${LIGHTSD_SOURCE_DIR}/core/utils.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../tests_shims.c
-+    ${CMAKE_CURRENT_SOURCE_DIR}/../../core/tests_utils.c
-+)
-+
-+FUNCTION(ADD_BROADCAST_TEST TEST_SOURCE)
-+    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_broadcast)
-+ENDFUNCTION()
-+
-+FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
-+FOREACH(TEST ${TESTS})
-+    ADD_BROADCAST_TEST(${TEST})
-+ENDFOREACH()
-diff --git a/tests/lifx/broadcast/test_broadcast_send_packet.c b/tests/lifx/broadcast/test_broadcast_send_packet.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/broadcast/test_broadcast_send_packet.c
-@@ -0,0 +1,146 @@
-+#include <sys/socket.h>
-+
-+ssize_t mock_sendto(int, const void *, size_t, int,
-+                    const struct sockaddr *, socklen_t);
-+
-+#define sendto(socket, buffer, length, flags, dest_addr, dest_len) \
-+    mock_sendto(socket, buffer, length, flags, dest_addr, dest_len)
-+
-+#include "broadcast.c"
-+
-+#include "mock_bulb.h"
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_log.h"
-+#include "mock_tagging.h"
-+#include "mock_wire_proto.h"
-+
-+#include "tests_utils.h"
-+
-+enum { MOCK_SOCKET_FD = 32 };
-+static const char MOCK_PKT[] = "fake packet";
-+static const struct sockaddr_in MOCK_ADDR = {
-+    .sin_family = AF_INET,
-+    .sin_addr = { INADDR_BROADCAST },
-+    .sin_port = LGTD_STATIC_HTONS(LGTD_LIFX_PROTOCOL_PORT),
-+    .sin_zero = { 0 }
-+};
-+
-+enum mock_sendto_calls {
-+    TEST_SENDTO_OK = 0,
-+    TEST_SENDTO_PARTIAL,
-+    TEST_SENDTO_EINTR,
-+    TEST_SENDTO_EINTR_OK,
-+    TEST_SENDTO_ERROR,
-+};
-+
-+static int mock_sendto_call_count = 0;
-+
-+ssize_t
-+mock_sendto(int socket,
-+            const void *buffer,
-+            size_t length,
-+            int flags,
-+            const struct sockaddr *dest_addr,
-+            socklen_t dest_len)
-+{
-+    if (socket != MOCK_SOCKET_FD) {
-+        lgtd_errx(
-+            1, "got socket=%jd (expected %d)", (intmax_t)socket, MOCK_SOCKET_FD
-+        );
-+    }
-+
-+    if (length != sizeof(MOCK_PKT)) {
-+        lgtd_errx(
-+            1, "got length=%ju (expected %ju)",
-+            (uintmax_t)length, (uintmax_t)sizeof(MOCK_PKT)
-+        );
-+    }
-+
-+    if (strcmp((const char *)buffer, MOCK_PKT)) {
-+        lgtd_errx(
-+            1, "got buffer [%.*s] (expected [%s])",
-+            (int)length, (const char *)buffer, MOCK_PKT
-+        );
-+    }
-+
-+    if (flags) {
-+        lgtd_errx(1, "got flags=%#x (expected 0x0)", flags);
-+    }
-+
-+    lgtd_tests_check_sockaddr_in(
-+        dest_addr, dest_len, AF_INET, INADDR_BROADCAST, LGTD_LIFX_PROTOCOL_PORT
-+    );
-+
-+    switch (mock_sendto_call_count++) {
-+    case TEST_SENDTO_OK:
-+        return length;
-+    case TEST_SENDTO_PARTIAL:
-+        return length - 1;
-+    case TEST_SENDTO_EINTR:
-+        errno = EINTR;
-+        return -1;
-+    case TEST_SENDTO_EINTR_OK:
-+        return length;
-+    case TEST_SENDTO_ERROR:
-+        errno = EIO;
-+        return -1;
-+    default:
-+        lgtd_errx(1, "sendto was called too many times");
-+    }
-+}
-+
-+int
-+main(void)
-+{
-+    lgtd_lifx_broadcast_endpoint.socket = MOCK_SOCKET_FD;
-+    const struct sockaddr *addr = (const struct sockaddr *)&MOCK_ADDR;
-+
-+    bool ok;
-+
-+    // ok
-+    ok = lgtd_lifx_broadcast_send_packet(
-+        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
-+    );
-+    if (mock_sendto_call_count != 1) {
-+        lgtd_errx(1, "sendto wasn't called");
-+    }
-+    if (!ok) {
-+        lgtd_errx(1, "broadcast_send_packet returned false (expected true)");
-+    }
-+
-+    // partial
-+    ok = lgtd_lifx_broadcast_send_packet(
-+        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
-+    );
-+    if (mock_sendto_call_count != 2) {
-+        lgtd_errx(1, "sendto wasn't called");
-+    }
-+    if (ok) {
-+        lgtd_errx(1, "broadcast_send_packet returned true (expected false)");
-+    }
-+
-+    // eintr
-+    ok = lgtd_lifx_broadcast_send_packet(
-+        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
-+    );
-+    if (mock_sendto_call_count != 4) {
-+        lgtd_errx(1, "sendto wasn't called");
-+    }
-+    if (!ok) {
-+        lgtd_errx(1, "broadcast_send_packet returned false (expected true)");
-+    }
-+
-+    // error
-+    ok = lgtd_lifx_broadcast_send_packet(
-+        MOCK_PKT, sizeof(MOCK_PKT), addr, sizeof(MOCK_ADDR)
-+    );
-+    if (mock_sendto_call_count != 5) {
-+        lgtd_errx(1, "sendto wasn't called");
-+    }
-+    if (ok) {
-+        lgtd_errx(1, "broadcast_send_packet returned true (expected false)");
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/broadcast/test_broadcast_write_callback.c b/tests/lifx/broadcast/test_broadcast_write_callback.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/broadcast/test_broadcast_write_callback.c
-@@ -0,0 +1,368 @@
-+#include <sys/types.h>
-+#include <sys/socket.h>
-+#include <ifaddrs.h>
-+
-+int mock_getifaddrs(struct ifaddrs **);
-+void mock_freeifaddrs(struct ifaddrs *);
-+ssize_t mock_sendto(int, const void *, size_t, int,
-+                    const struct sockaddr *, socklen_t);
-+
-+#define getifaddrs(ifap) mock_getifaddrs(ifap)
-+#define freeifaddrs(ifp) mock_freeifaddrs(ifp)
-+#define sendto(socket, buffer, length, flags, dest_addr, dest_len) \
-+    mock_sendto(socket, buffer, length, flags, dest_addr, dest_len)
-+
-+#include "broadcast.c"
-+
-+#include "mock_bulb.h"
-+#define MOCKED_EVENT_DEL
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_log.h"
-+#include "mock_tagging.h"
-+#define MOCKED_LGTD_LIFX_WIRE_SETUP_HEADER
-+#include "mock_wire_proto.h"
-+
-+#include "tests_utils.h"
-+
-+enum { MOCK_SOCKET_FD = 32 };
-+static struct event *MOCK_WRITE_EV = (struct event *)0x7061726973;
-+
-+// /24
-+#define TEST_IPV4_NETMASK_ADDR_24 LGTD_STATIC_HTONL(0xffffff00)
-+static struct sockaddr_in TEST_IPV4_NETMASK_SOCKADDR_24 = {
-+    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_NETMASK_ADDR_24 }
-+};
-+
-+// /16
-+#define TEST_IPV4_NETMASK_ADDR_16 LGTD_STATIC_HTONL(0xffff0000)
-+static struct sockaddr_in TEST_IPV4_NETMASK_SOCKADDR_16 = {
-+    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_NETMASK_ADDR_16 }
-+};
-+
-+// 192.168.42.255
-+#define TEST_IPV4_BROADCAST_ADDR_CLASS_C LGTD_STATIC_HTONL(0xc0a82aff)
-+static struct sockaddr_in TEST_IPV4_BROADCAST_SOCKADDR_CLASS_C = {
-+    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_BROADCAST_ADDR_CLASS_C }
-+};
-+
-+// 10.10.255.255
-+#define TEST_IPV4_BROADCAST_ADDR_CLASS_A LGTD_STATIC_HTONL(0x0a0affff)
-+static struct sockaddr_in TEST_IPV4_BROADCAST_SOCKADDR_CLASS_A = {
-+    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_BROADCAST_ADDR_CLASS_A }
-+};
-+
-+// 82.66.148.158
-+#define TEST_IPV4_UNICAST_ADDR_ROUTABLE LGTD_STATIC_HTONL(0x5242949e)
-+static struct sockaddr_in TEST_IPV4_UNICAST_SOCKADDR_ROUTABLE = {
-+    .sin_family = AF_INET, .sin_addr = { TEST_IPV4_UNICAST_ADDR_ROUTABLE }
-+};
-+
-+static struct sockaddr_in6 TEST_IPV6_NETMASK_SOCKADDR_64 = {
-+    .sin6_family = AF_INET6
-+};
-+static struct sockaddr_in6 TEST_IPV6_UNICAST_SOCKADDR = {
-+    .sin6_family = AF_INET6
-+};
-+
-+static struct ifaddrs MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_C = {
-+    .ifa_flags = IFF_BROADCAST,
-+    .ifa_broadaddr = (struct sockaddr *)&TEST_IPV4_BROADCAST_SOCKADDR_CLASS_C,
-+    .ifa_netmask = (struct sockaddr *)&TEST_IPV4_NETMASK_SOCKADDR_24,
-+    .ifa_next = NULL
-+};
-+static struct ifaddrs MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_A = {
-+    .ifa_flags = IFF_BROADCAST,
-+    .ifa_broadaddr = (struct sockaddr *)&TEST_IPV4_BROADCAST_SOCKADDR_CLASS_A,
-+    .ifa_netmask = (struct sockaddr *)&TEST_IPV4_NETMASK_SOCKADDR_16,
-+    .ifa_next = &MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_C
-+};
-+static struct ifaddrs MOCK_IFSOCKADDR_IPV4_UNICAST_SOCKADDR_ROUTABLE = {
-+    .ifa_dstaddr = (struct sockaddr *)&TEST_IPV4_UNICAST_SOCKADDR_ROUTABLE,
-+    .ifa_netmask = (struct sockaddr *)&TEST_IPV4_NETMASK_SOCKADDR_24,
-+    .ifa_next = &MOCK_IFSOCKADDR_IPV4_BROADCAST_SOCKADDR_CLASS_A
-+};
-+static struct ifaddrs MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR = {
-+    .ifa_dstaddr = (struct sockaddr *)&TEST_IPV6_UNICAST_SOCKADDR,
-+    .ifa_netmask = (struct sockaddr *)&TEST_IPV6_NETMASK_SOCKADDR_64,
-+    .ifa_next = &MOCK_IFSOCKADDR_IPV4_UNICAST_SOCKADDR_ROUTABLE
-+};
-+
-+static int event_del_call_count = 0;
-+
-+int
-+event_del(struct event *ev)
-+{
-+    if (ev != MOCK_WRITE_EV) {
-+        lgtd_errx(
-+            1, "event_del received invalid event=%p (expected %p)",
-+            ev, MOCK_WRITE_EV
-+        );
-+    }
-+
-+    event_del_call_count++;
-+
-+    return 0;
-+}
-+
-+static int mock_getifaddrs_call_count = 0;
-+
-+int
-+mock_getifaddrs(struct ifaddrs **ifap)
-+{
-+    if (!ifap) {
-+        lgtd_errx(1, "ifap souldn't be NULL");
-+    }
-+
-+    *ifap = &MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR;
-+
-+    mock_getifaddrs_call_count++;
-+
-+    return 0;
-+}
-+
-+static int mock_freeifaddrs_call_count = 0;
-+
-+void
-+mock_freeifaddrs(struct ifaddrs *ifp)
-+{
-+    if (ifp != &MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR) {
-+        lgtd_errx(
-+            1, "got ifp = %p (expected %p)",
-+            ifp, &MOCK_IFSOCKADDR_IPV6_UNICAST_SOCKADDR
-+        );
-+    }
-+
-+    mock_freeifaddrs_call_count++;
-+}
-+
-+static int mock_lifx_wire_setup_header_call_count = 0;
-+
-+const struct lgtd_lifx_packet_info *
-+lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *hdr,
-+                            enum lgtd_lifx_target_type target_type,
-+                            union lgtd_lifx_target target,
-+                            const uint8_t *site,
-+                            enum lgtd_lifx_packet_type packet_type)
-+{
-+    if (!hdr) {
-+        lgtd_errx(1, "missing header");
-+    }
-+
-+    if (target_type != LGTD_LIFX_TARGET_ALL_DEVICES) {
-+        lgtd_errx(
-+            1, "got target type %d (expected %d)",
-+            target_type, LGTD_LIFX_TARGET_ALL_DEVICES
-+        );
-+    }
-+
-+    if (memcmp(&target, &LGTD_LIFX_UNSPEC_TARGET, sizeof(target))) {
-+        lgtd_errx(
-+            1, "got unexpected target (expected LGTD_LIFX_UNSPEC_TARGET)"
-+        );
-+    }
-+
-+    if (site) {
-+        lgtd_errx(1, "got unexpected site %p (expected NULL)", site);
-+    }
-+
-+    if (packet_type != LGTD_LIFX_GET_PAN_GATEWAY) {
-+        lgtd_errx(
-+            1, "got unexpected packet type %d (expected %d)",
-+            packet_type, LGTD_LIFX_GET_PAN_GATEWAY
-+        );
-+    }
-+
-+    mock_lifx_wire_setup_header_call_count++;
-+
-+    return NULL;
-+}
-+
-+static int mock_sendto_call_count = 0;
-+
-+enum mock_sendto_calls {
-+    // 1st test case everything ok
-+    TEST_OK_SENDTO_OK_ADDR_CLASS_A = 0,
-+    TEST_OK_SENDTO_OK_ADDR_CLASS_C,
-+    // 2nd test case one failure
-+    TEST_PARTIAL_SENDTO_OK_ADDR_CLASS_A,
-+    TEST_PARTIAL_SENDTO_ERROR_ADDR_CLASS_C,
-+    // 3rd test case all failures
-+    TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_A,
-+    TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_C
-+};
-+
-+ssize_t
-+mock_sendto(int socket,
-+            const void *buffer,
-+            size_t length,
-+            int flags,
-+            const struct sockaddr *dest_addr,
-+            socklen_t dest_len)
-+{
-+    if (socket != MOCK_SOCKET_FD) {
-+        lgtd_errx(
-+            1, "got socket=%jd (expected %d)", (intmax_t)socket, MOCK_SOCKET_FD
-+        );
-+    }
-+
-+    if (!buffer) {
-+        lgtd_errx(1, "missing buffer");
-+    }
-+
-+    if (length != sizeof(struct lgtd_lifx_packet_header)) {
-+        lgtd_errx(
-+            1, "got buffer length=%ju (expected %ju)",
-+            (uintmax_t)length, (uintmax_t)sizeof(struct lgtd_lifx_packet_header)
-+        );
-+    }
-+
-+    if (flags) {
-+        lgtd_errx(1, "got flags=%#x (expected 0x0)", flags);
-+    }
-+
-+    switch (mock_sendto_call_count++) {
-+    case TEST_PARTIAL_SENDTO_OK_ADDR_CLASS_A:
-+    case TEST_OK_SENDTO_OK_ADDR_CLASS_A:
-+        lgtd_tests_check_sockaddr_in(
-+            dest_addr,
-+            dest_len,
-+            AF_INET,
-+            TEST_IPV4_BROADCAST_ADDR_CLASS_A,
-+            LGTD_LIFX_PROTOCOL_PORT
-+        );
-+        return length;
-+    case TEST_OK_SENDTO_OK_ADDR_CLASS_C:
-+        lgtd_tests_check_sockaddr_in(
-+            dest_addr,
-+            dest_len,
-+            AF_INET,
-+            TEST_IPV4_BROADCAST_ADDR_CLASS_C,
-+            LGTD_LIFX_PROTOCOL_PORT
-+        );
-+        return length;
-+    case TEST_PARTIAL_SENDTO_ERROR_ADDR_CLASS_C:
-+    case TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_C:
-+        lgtd_tests_check_sockaddr_in(
-+            dest_addr,
-+            dest_len,
-+            AF_INET,
-+            TEST_IPV4_BROADCAST_ADDR_CLASS_C,
-+            LGTD_LIFX_PROTOCOL_PORT
-+        );
-+        return length - 1;
-+    case TEST_FAIL_SENDTO_ERROR_ADDR_CLASS_A:
-+        lgtd_tests_check_sockaddr_in(
-+            dest_addr,
-+            dest_len,
-+            AF_INET,
-+            TEST_IPV4_BROADCAST_ADDR_CLASS_A,
-+            LGTD_LIFX_PROTOCOL_PORT
-+        );
-+        return length - 1;
-+    default:
-+        lgtd_errx(1, "mock_sendto called too many times");
-+    }
-+}
-+
-+int
-+main(void)
-+{
-+    lgtd_lifx_broadcast_endpoint.socket = MOCK_SOCKET_FD;
-+    lgtd_lifx_broadcast_endpoint.write_ev = MOCK_WRITE_EV;
-+
-+    bool ok;
-+
-+    // getifaddrs ok
-+    ok = lgtd_lifx_broadcast_handle_write();
-+    if (!ok) {
-+        lgtd_errx(1, "write callback returned false (expected true)");
-+    }
-+    if (mock_lifx_wire_setup_header_call_count != 1) {
-+        lgtd_errx(
-+            1, "mock_lifx_wire_setup_header_call_count=%d (expected 1)",
-+            mock_lifx_wire_setup_header_call_count
-+        );
-+    }
-+    if (mock_sendto_call_count != 2) {
-+        lgtd_errx(
-+            1, "mock_sendto_call_count=%d (expected 2)",
-+            mock_sendto_call_count
-+        );
-+    }
-+    if (event_del_call_count != 1) {
-+        lgtd_errx(
-+            1, "event_del_call_count=%d (expected 1)", event_del_call_count
-+        );
-+    }
-+    if (mock_freeifaddrs_call_count != 1) {
-+        lgtd_errx(
-+            1, "freeifaddrs_call_count=%d (expected 1)",
-+            mock_freeifaddrs_call_count
-+        );
-+    }
-+
-+    lgtd_tests_hr();
-+
-+    // getifaddrs ok, one send fails
-+    ok = lgtd_lifx_broadcast_handle_write();
-+    if (!ok) {
-+        lgtd_errx(1, "write callback returned false (expected true)");
-+    }
-+    if (mock_lifx_wire_setup_header_call_count != 2) {
-+        lgtd_errx(
-+            1, "mock_lifx_wire_setup_header_call_count=%d (expected 2)",
-+            mock_lifx_wire_setup_header_call_count
-+        );
-+    }
-+    if (mock_sendto_call_count != 4) {
-+        lgtd_errx(
-+            1, "mock_sendto_call_count=%d (expected 4)",
-+            mock_sendto_call_count
-+        );
-+    }
-+    if (event_del_call_count != 2) {
-+        lgtd_errx(
-+            1, "event_del_call_count=%d (expected 2)", event_del_call_count
-+        );
-+    }
-+    if (mock_freeifaddrs_call_count != 2) {
-+        lgtd_errx(
-+            1, "freeifaddrs_call_count=%d (expected 2)",
-+            mock_freeifaddrs_call_count
-+        );
-+    }
-+
-+    lgtd_tests_hr();
-+
-+    // getifaddrs ok, all sends fail
-+    ok = lgtd_lifx_broadcast_handle_write();
-+    if (ok) {
-+        lgtd_errx(1, "write callback returned true (expected false)");
-+    }
-+    if (mock_lifx_wire_setup_header_call_count != 3) {
-+        lgtd_errx(
-+            1, "mock_lifx_wire_setup_header_call_count=%d (expected 2)",
-+            mock_lifx_wire_setup_header_call_count
-+        );
-+    }
-+    if (mock_sendto_call_count != 6) {
-+        lgtd_errx(
-+            1, "mock_sendto_call_count=%d (expected 6)",
-+            mock_sendto_call_count
-+        );
-+    }
-+    if (event_del_call_count != 2) {
-+        lgtd_errx(
-+            1, "event_del_call_count=%d (expected 2)", event_del_call_count
-+        );
-+    }
-+    if (mock_freeifaddrs_call_count != 3) {
-+        lgtd_errx(
-+            1, "freeifaddrs_call_count=%d (expected 3)",
-+            mock_freeifaddrs_call_count
-+        );
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/broadcast/test_broadcast_write_callback_getifaddrs_fails.c b/tests/lifx/broadcast/test_broadcast_write_callback_getifaddrs_fails.c
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/broadcast/test_broadcast_write_callback_getifaddrs_fails.c
-@@ -0,0 +1,222 @@
-+#include <sys/types.h>
-+#include <sys/socket.h>
-+#include <ifaddrs.h>
-+
-+int mock_getifaddrs(struct ifaddrs **);
-+void mock_freeifaddrs(struct ifaddrs *);
-+ssize_t mock_sendto(int, const void *, size_t, int,
-+                    const struct sockaddr *, socklen_t);
-+
-+#define getifaddrs(ifap) mock_getifaddrs(ifap)
-+#define freeifaddrs(ifp) mock_freeifaddrs(ifp)
-+#define sendto(socket, buffer, length, flags, dest_addr, dest_len) \
-+    mock_sendto(socket, buffer, length, flags, dest_addr, dest_len)
-+
-+#include "broadcast.c"
-+
-+#include "mock_bulb.h"
-+#define MOCKED_EVENT_DEL
-+#include "mock_event2.h"
-+#include "mock_gateway.h"
-+#include "mock_log.h"
-+#include "mock_tagging.h"
-+#define MOCKED_LGTD_LIFX_WIRE_SETUP_HEADER
-+#include "mock_wire_proto.h"
-+
-+#include "tests_utils.h"
-+
-+enum { MOCK_SOCKET_FD = 32 };
-+static struct event *MOCK_WRITE_EV = (struct event *)0x7061726973;
-+static const struct sockaddr_in LIFX_BROADCAST_ADDR = {
-+    .sin_family = AF_INET,
-+    .sin_addr = { INADDR_BROADCAST },
-+    .sin_port = LGTD_STATIC_HTONS(LGTD_LIFX_PROTOCOL_PORT),
-+    .sin_zero = { 0 }
-+};
-+
-+static int event_del_call_count = 0;
-+
-+int
-+event_del(struct event *ev)
-+{
-+    if (ev != MOCK_WRITE_EV) {
-+        lgtd_errx(
-+            1, "event_del received invalid event=%p (expected %p)",
-+            ev, MOCK_WRITE_EV
-+        );
-+    }
-+
-+    event_del_call_count++;
-+
-+    return 0;
-+}
-+
-+int
-+mock_getifaddrs(struct ifaddrs **ifap)
-+{
-+    if (!ifap) {
-+        lgtd_errx(1, "ifap souldn't be NULL");
-+    }
-+
-+    errno = ENOSYS;
-+
-+    return -1;
-+}
-+
-+void
-+mock_freeifaddrs(struct ifaddrs *ifp)
-+{
-+    (void)ifp;
-+
-+    lgtd_errx(1, "freeifaddrs shouldn't have been called");
-+}
-+
-+static int mock_lifx_wire_setup_header_call_count = 0;
-+
-+const struct lgtd_lifx_packet_info *
-+lgtd_lifx_wire_setup_header(struct lgtd_lifx_packet_header *hdr,
-+                            enum lgtd_lifx_target_type target_type,
-+                            union lgtd_lifx_target target,
-+                            const uint8_t *site,
-+                            enum lgtd_lifx_packet_type packet_type)
-+{
-+    if (!hdr) {
-+        lgtd_errx(1, "missing header");
-+    }
-+
-+    if (target_type != LGTD_LIFX_TARGET_ALL_DEVICES) {
-+        lgtd_errx(
-+            1, "got target type %d (expected %d)",
-+            target_type, LGTD_LIFX_TARGET_ALL_DEVICES
-+        );
-+    }
-+
-+    if (memcmp(&target, &LGTD_LIFX_UNSPEC_TARGET, sizeof(target))) {
-+        lgtd_errx(
-+            1, "got unexpected target (expected LGTD_LIFX_UNSPEC_TARGET)"
-+        );
-+    }
-+
-+    if (site) {
-+        lgtd_errx(1, "got unexpected site %p (expected NULL)", site);
-+    }
-+
-+    if (packet_type != LGTD_LIFX_GET_PAN_GATEWAY) {
-+        lgtd_errx(
-+            1, "got unexpected packet type %d (expected %d)",
-+            packet_type, LGTD_LIFX_GET_PAN_GATEWAY
-+        );
-+    }
-+
-+    mock_lifx_wire_setup_header_call_count++;
-+
-+    return NULL;
-+}
-+
-+enum mock_sendto_calls {
-+    MOCK_SENDTO_OK = 0,
-+    MOCK_SENDTO_ERROR,
-+};
-+
-+static int mock_sendto_call_count = 0;
-+
-+ssize_t
-+mock_sendto(int socket,
-+            const void *buffer,
-+            size_t length,
-+            int flags,
-+            const struct sockaddr *dest_addr,
-+            socklen_t dest_len)
-+{
-+    if (socket != MOCK_SOCKET_FD) {
-+        lgtd_errx(
-+            1, "got socket=%jd (expected %d)", (intmax_t)socket, MOCK_SOCKET_FD
-+        );
-+    }
-+
-+    if (!buffer) {
-+        lgtd_errx(1, "missing buffer");
-+    }
-+
-+    if (length != sizeof(struct lgtd_lifx_packet_header)) {
-+        lgtd_errx(
-+            1, "got buffer length=%ju (expected %ju)",
-+            (uintmax_t)length, (uintmax_t)sizeof(struct lgtd_lifx_packet_header)
-+        );
-+    }
-+
-+    if (flags) {
-+        lgtd_errx(1, "got flags=%#x (expected 0x0)", flags);
-+    }
-+
-+    lgtd_tests_check_sockaddr_in(
-+        dest_addr, dest_len, AF_INET, INADDR_BROADCAST, LGTD_LIFX_PROTOCOL_PORT
-+    );
-+
-+    switch (mock_sendto_call_count++) {
-+    case MOCK_SENDTO_OK:
-+        return length;
-+    case MOCK_SENDTO_ERROR:
-+        errno = EIO;
-+        return -1;
-+    default:
-+        lgtd_errx(1, "sendto was called too many times");
-+    }
-+}
-+
-+int
-+main(void)
-+{
-+    lgtd_lifx_broadcast_endpoint.socket = MOCK_SOCKET_FD;
-+    lgtd_lifx_broadcast_endpoint.write_ev = MOCK_WRITE_EV;
-+
-+    bool ok;
-+
-+    // getifaddrs fails
-+    ok = lgtd_lifx_broadcast_handle_write();
-+    if (!ok) {
-+        lgtd_errx(1, "write callback returned false (expected true)");
-+    }
-+    if (mock_lifx_wire_setup_header_call_count != 1) {
-+        lgtd_errx(
-+            1, "mock_lifx_wire_setup_header_call_count=%d (expected 1)",
-+            mock_lifx_wire_setup_header_call_count
-+        );
-+    }
-+    if (mock_sendto_call_count != 1) {
-+        lgtd_errx(
-+            1, "mock_sendto_call_count=%d (expected 1)",
-+            mock_sendto_call_count
-+        );
-+    }
-+    if (event_del_call_count != 1) {
-+        lgtd_errx(
-+            1, "event_del_call_count=%d (expected 1)", event_del_call_count
-+        );
-+    }
-+
-+    // getifaddrs & lgtd_lifx_broadcast_send_packet fail
-+    ok = lgtd_lifx_broadcast_handle_write();
-+    if (ok) {
-+        lgtd_errx(1, "write callback returned true (expected false)");
-+    }
-+    if (mock_lifx_wire_setup_header_call_count != 2) {
-+        lgtd_errx(
-+            1, "mock_lifx_wire_setup_header_call_count=%d (expected 2)",
-+            mock_lifx_wire_setup_header_call_count
-+        );
-+    }
-+    if (mock_sendto_call_count != 2) {
-+        lgtd_errx(
-+            1, "mock_sendto_call_count=%d (expected 2)",
-+            mock_sendto_call_count
-+        );
-+    }
-+    if (event_del_call_count != 1) {
-+        lgtd_errx(
-+            1, "event_del_call_count=%d (expected 1)", event_del_call_count
-+        );
-+    }
-+
-+    return 0;
-+}
-diff --git a/tests/lifx/mock_bulb.h b/tests/lifx/mock_bulb.h
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/mock_bulb.h
-@@ -0,0 +1,138 @@
-+#pragma once
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_GET
-+struct lgtd_lifx_bulb *
-+lgtd_lifx_bulb_get(const uint8_t *addr)
-+{
-+    (void)addr;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_OPEN
-+struct lgtd_lifx_bulb *
-+lgtd_lifx_bulb_open(struct lgtd_lifx_gateway *gw, const uint8_t *addr)
-+{
-+    (void)gw;
-+    (void)addr;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_CLOSE
-+void
-+lgtd_lifx_bulb_close(struct lgtd_lifx_bulb *bulb)
-+{
-+    (void)bulb;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_HAS_LABEL
-+bool
-+lgtd_lifx_bulb_has_label(const struct lgtd_lifx_bulb *bulb,
-+                         const char *label)
-+{
-+    (void)bulb;
-+    (void)label;
-+    return false;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_LIGHT_STATE
-+void
-+lgtd_lifx_bulb_set_light_state(struct lgtd_lifx_bulb *bulb,
-+                               const struct lgtd_lifx_light_state *state,
-+                               lgtd_time_mono_t received_at)
-+{
-+    (void)bulb;
-+    (void)state;
-+    (void)received_at;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_POWER_STATE
-+void
-+lgtd_lifx_bulb_set_power_state(struct lgtd_lifx_bulb *bulb, uint16_t power)
-+{
-+    (void)bulb;
-+    (void)power;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_TAGS
-+void
-+lgtd_lifx_bulb_set_tags(struct lgtd_lifx_bulb *bulb, uint64_t tags)
-+{
-+    (void)bulb;
-+    (void)tags;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_IP_STATE
-+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)bulb;
-+    (void)ip_id;
-+    (void)state;
-+    (void)received_at;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_IP_FIRMWARE_INFO
-+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)bulb;
-+    (void)ip_id;
-+    (void)info;
-+    (void)received_at;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_PRODUCT_INFO
-+void
-+lgtd_lifx_bulb_set_product_info(struct lgtd_lifx_bulb *bulb,
-+                                const struct lgtd_lifx_product_info *info)
-+{
-+    (void)bulb;
-+    (void)info;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_RUNTIME_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)
-+{
-+    (void)bulb;
-+    (void)info;
-+    (void)received_at;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_LABEL
-+void
-+lgtd_lifx_bulb_set_label(struct lgtd_lifx_bulb *bulb,
-+                         const char label[LGTD_LIFX_LABEL_SIZE])
-+{
-+    (void)bulb;
-+    (void)label;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_BULB_SET_AMBIENT_LIGHT
-+void
-+lgtd_lifx_bulb_set_ambient_light(struct lgtd_lifx_bulb *bulb, float illuminance)
-+{
-+    (void)bulb;
-+    (void)illuminance;
-+}
-+#endif
-diff --git a/tests/lifx/mock_tagging.h b/tests/lifx/mock_tagging.h
-new file mode 100644
---- /dev/null
-+++ b/tests/lifx/mock_tagging.h
-@@ -0,0 +1,55 @@
-+#pragma once
-+
-+#include "lifx/tagging.h"
-+
-+struct lgtd_lifx_tag_list lgtd_lifx_tags =
-+    LIST_HEAD_INITIALIZER(&lgtd_lifx_tags);
-+
-+#ifndef MOCKED_LGTD_LIFX_TAGGING_FIND_TAG
-+struct lgtd_lifx_tag *
-+lgtd_lifx_tagging_find_tag(const char *tag_label)
-+{
-+    (void)tag_label;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_TAGGING_ALLOCATE_TAG
-+struct lgtd_lifx_tag *
-+lgtd_lifx_tagging_allocate_tag(const char *tag_label)
-+{
-+    (void)tag_label;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_TAGGING_DEALLOCATE_TAG
-+void
-+lgtd_lifx_tagging_deallocate_tag(struct lgtd_lifx_tag *tag)
-+{
-+    (void)tag;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_TAGGING_INCREF
-+struct lgtd_lifx_tag *
-+lgtd_lifx_tagging_incref(const char *tag_label,
-+                         struct lgtd_lifx_gateway *gw,
-+                         int tag_id)
-+{
-+    (void)tag_label;
-+    (void)gw;
-+    (void)tag_id;
-+    return NULL;
-+}
-+#endif
-+
-+#ifndef MOCKED_LGTD_LIFX_TAGGING_DECREF
-+void
-+lgtd_lifx_tagging_decref(struct lgtd_lifx_tag *tag,
-+                         struct lgtd_lifx_gateway *gw)
-+{
-+    (void)tag;
-+    (void)gw;
-+}
-+#endif
-diff --git a/tests/lifx/wire_proto/CMakeLists.txt b/tests/lifx/wire_proto/CMakeLists.txt
---- a/tests/lifx/wire_proto/CMakeLists.txt
-+++ b/tests/lifx/wire_proto/CMakeLists.txt
-@@ -4,12 +4,12 @@
- )
- 
- ADD_CORE_LIBRARY(
--    test_lifx_wire_proto_core STATIC
-+    test_lifx_wire_proto STATIC
-     ${LIGHTSD_SOURCE_DIR}/core/utils.c
- )
- 
- FUNCTION(ADD_WIRE_PROTO_TEST TEST_SOURCE)
--    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto_core)
-+    ADD_TEST_FROM_C_SOURCES(${TEST_SOURCE} test_lifx_wire_proto)
- ENDFUNCTION()
- 
- FILE(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "test_*.c")
--- a/optional_jsonrpc_args.patch	Sat May 28 18:42:04 2016 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,644 +0,0 @@
-# HG changeset patch
-# Parent  f53fd0ef32c475da14c00890cf0b2d0f65bc1e9a
-Correctly support optional arguments in the JSON-RPC API
-
-Passing too many arguments as an array also properly fails now.
-
-While we are at it, put the correct argument names in the documentation
-so that it works for people passing argument by name.
-
-diff --git a/core/jsonrpc.c b/core/jsonrpc.c
---- a/core/jsonrpc.c
-+++ b/core/jsonrpc.c
-@@ -459,7 +459,7 @@
-         }
-     }
- 
--    return si == schema_size;
-+    return !objsize || (si < schema_size && schema[si].optional);
- }
- 
- static bool
-@@ -470,6 +470,17 @@
-                                                         int ntokens,
-                                                         const char *json)
- {
-+    if (!ntokens) {
-+        // "params" were omitted, make sure no args were required or that they
-+        // are all optional:
-+        while (schema_size--) {
-+            if (!schema[schema_size].optional) {
-+                return false;
-+            }
-+        }
-+        return true;
-+    }
-+
-     switch (tokens[0].type) {
-     case JSMN_OBJECT:
-         return lgtd_jsonrpc_extract_values_from_schema_and_dict(
-@@ -716,7 +727,7 @@
-             offsetof(struct lgtd_jsonrpc_set_light_from_hsbk_args, t),
-             -1,
-             lgtd_jsonrpc_type_integer,
--            false
-+            true
-         ),
-     };
- 
-@@ -746,9 +757,12 @@
-     if (k < 2500 || k > 9000 || errno == ERANGE) {
-         goto error_invalid_params;
-     }
--    int t = strtol(&client->json[params.t->start], NULL, 10);
--    if (t < 0 || errno == ERANGE) {
--        goto error_invalid_params;
-+    int t = 0;
-+    if (params.t) {
-+        t = strtol(&client->json[params.t->start], NULL, 10);
-+        if (t < 0 || errno == ERANGE) {
-+            goto error_invalid_params;
-+        }
-     }
- 
-     struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets);
-@@ -854,7 +868,7 @@
-             offsetof(struct lgtd_jsonrpc_set_waveform_args, transient),
-             -1,
-             lgtd_jsonrpc_type_bool,
--            false
-+            true
-         ),
-     };
- 
-@@ -906,7 +920,8 @@
-         0, 1
-     );
-     skew_ratio -= UINT16_MAX / 2;
--    bool transient = client->json[params.transient->start] == 't';
-+    bool transient = params.transient ?
-+        client->json[params.transient->start] == 't' : true;
- 
-     struct lgtd_proto_target_list targets = SLIST_HEAD_INITIALIZER(&targets);
-     ok = lgtd_jsonrpc_build_target_list(
-@@ -1102,43 +1117,24 @@
-                           int *batch_sent)
- {
-     static const struct lgtd_jsonrpc_method methods[] = {
-+        LGTD_JSONRPC_METHOD("power_on", lgtd_jsonrpc_check_and_call_power_on),
-+        LGTD_JSONRPC_METHOD("power_off", lgtd_jsonrpc_check_and_call_power_off),
-         LGTD_JSONRPC_METHOD(
--            "power_on", 1, // t
--            lgtd_jsonrpc_check_and_call_power_on
-+            "power_toggle", lgtd_jsonrpc_check_and_call_power_toggle
-         ),
-         LGTD_JSONRPC_METHOD(
--            "power_off", 1, // t
--            lgtd_jsonrpc_check_and_call_power_off
--        ),
--        LGTD_JSONRPC_METHOD(
--            "power_toggle", 1, // t
--            lgtd_jsonrpc_check_and_call_power_toggle
--        ),
--        LGTD_JSONRPC_METHOD(
--            "set_light_from_hsbk", 6, // t, h, s, b, k, t
-+            "set_light_from_hsbk",
-             lgtd_jsonrpc_check_and_call_set_light_from_hsbk
-         ),
-         LGTD_JSONRPC_METHOD(
--            // t, waveform, h, s, b, k, period, cycles, skew_ratio, transient
--            "set_waveform", 10,
--            lgtd_jsonrpc_check_and_call_set_waveform
-+            "set_waveform", lgtd_jsonrpc_check_and_call_set_waveform
-         ),
-         LGTD_JSONRPC_METHOD(
--            "get_light_state", 1, // t
--            lgtd_jsonrpc_check_and_call_get_light_state
-+            "get_light_state", lgtd_jsonrpc_check_and_call_get_light_state
-         ),
--        LGTD_JSONRPC_METHOD(
--            "tag", 2, // t, tag
--            lgtd_jsonrpc_check_and_call_tag
--        ),
--        LGTD_JSONRPC_METHOD(
--            "untag", 2, // t, tag
--            lgtd_jsonrpc_check_and_call_untag
--        ),
--        LGTD_JSONRPC_METHOD(
--            "set_label", 2, // t, label
--            lgtd_jsonrpc_check_and_call_set_label
--        )
-+        LGTD_JSONRPC_METHOD("tag", lgtd_jsonrpc_check_and_call_tag),
-+        LGTD_JSONRPC_METHOD("untag", lgtd_jsonrpc_check_and_call_untag),
-+        LGTD_JSONRPC_METHOD("set_label", lgtd_jsonrpc_check_and_call_set_label)
-     };
- 
-     if (batch_sent) {
-@@ -1172,15 +1168,11 @@
-             continue;
-         }
-         int diff = memcmp(
--            methods[i].name, &client->json[request.method->start], methods[i].namelen
-+            methods[i].name,
-+            &client->json[request.method->start],
-+            methods[i].namelen
-         );
-         if (!diff) {
--            int params_count = request.params ? request.params->size : 0;
--            if (params_count != methods[i].params_count) {
--                error_code = LGTD_JSONRPC_INVALID_PARAMS;
--                error_msg = "Invalid number of parameters";
--                goto error;
--            }
-             struct bufferevent *client_io = NULL; // keep compilers happy...
-             if (!request.id) {
-                 // Ugly hack to behave correctly on jsonrpc notifications, it's
-diff --git a/core/jsonrpc.h b/core/jsonrpc.h
---- a/core/jsonrpc.h
-+++ b/core/jsonrpc.h
-@@ -63,15 +63,13 @@
- struct lgtd_jsonrpc_method {
-     const char  *name;
-     int         namelen;
--    int         params_count;
-     void        (*method)(struct lgtd_client *);
- };
- 
--#define LGTD_JSONRPC_METHOD(name_, params_count_, method_) {    \
--    .name = (name_),                                            \
--    .namelen = sizeof((name_)) -1,                              \
--    .params_count = (params_count_),                            \
--    .method = (method_)                                         \
-+#define LGTD_JSONRPC_METHOD(name_, method_) {   \
-+    .name = (name_),                            \
-+    .namelen = sizeof((name_)) -1,              \
-+    .method = (method_)                         \
- }
- 
- enum lgtd_jsonrpc_error_code {
-diff --git a/docs/protocol.rst b/docs/protocol.rst
---- a/docs/protocol.rst
-+++ b/docs/protocol.rst
-@@ -32,9 +32,7 @@
- +-----------------------------+------------------------------------------------+
- 
- The mac address (id) of each bulb can be found with get_light_state_ under the
--``_lifx`` map, e.g:
--
--::
-+``_lifx`` map, e.g::
- 
-    "_lifx": {
-        "addr": "d0:73:d5:02:e5:30",
-@@ -65,28 +63,30 @@
- 
-    Power on (if they are off) or power off (if they are on) the given bulb(s).
- 
--.. function:: set_light_from_hsbk(target, h, s, b, k, t)
-+.. function:: set_light_from_hsbk(target, hue, saturation, brightness, kelvin[, transition])
- 
--   :param float h: Hue from 0 to 360.
--   :param float s: Saturation from 0 to 1.
--   :param float b: Brightness from 0 to 1.
--   :param int k: Temperature in Kelvin from 2500 to 9000.
--   :param int t: Transition duration to this color in ms.
-+   :param float hue: From 0 to 360.
-+   :param float saturation: From 0 to 1.
-+   :param float brightness: From 0 to 1.
-+   :param int kelvin: Temperature in Kelvin from 2500 to 9000.
-+   :param int transition: Optional time in ms it will take for the bulb to turn
-+                          to this color.
- 
--.. function:: set_waveform(target, waveform, h, s, b, k, period, cycles, skew_ratio, transient)
-+.. function:: set_waveform(target, waveform, hue, saturation, brightness, kelvin, period, cycles, skew_ratio[, transient])
- 
-    :param string waveform: One of ``SAW``, ``SINE``, ``HALF_SINE``,
-                            ``TRIANGLE``, ``SQUARE``.
--   :param float h: Hue from 0 to 360.
--   :param float s: Saturation from 0 to 1.
--   :param float b: Brightness from 0 to 1.
--   :param int k: Temperature in Kelvin from 2500 to 9000.
--   :param int period: milliseconds per cycle.
--   :param int cycles: number of cycles.
--   :param float skew_ratio: from 0 to 1.
--   :param bool transient: if true the target will keep the color it has at the
-+   :param float hue: From 0 to 360.
-+   :param float saturation: From 0 to 1.
-+   :param float brightness: From 0 to 1.
-+   :param int kelvin: Temperature in Kelvin from 2500 to 9000.
-+   :param int period: Milliseconds per cycle.
-+   :param int cycles: Number of cycles.
-+   :param float skew_ratio: From 0 to 1, see table below.
-+   :param bool transient: If true the target will keep the color it has at the
-                           end of the waveform, otherwise it will revert back to
--                          its original state.
-+                          its original state. This argument is optional and
-+                          defaults to true.
- 
-    The meaning of the ``skew_ratio`` argument depends on the type of waveform:
- 
-@@ -129,9 +129,7 @@
-    Tag (group) the given target bulb(s) with the given label (group name), then
-    label can be used as a target by prefixing it with ``#``.
- 
--   To add a device to an existing "group" simply do:
--
--   ::
-+   To add a device to an existing "group" simply do::
- 
-       tag(["#myexistingtag", "bulbtoadd"], "myexistingtag")
- 
-@@ -143,9 +141,7 @@
- .. function:: untag(target, label)
- 
-    Remove the given tag from the given target bulb(s). To completely delete a
--   tag (group), simple do:
--
--   ::
-+   tag (group), simply do::
- 
-       untag("#myexistingtag", "myexistingtag")
- 
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk.c
-@@ -8,7 +8,7 @@
- 
- #include "test_jsonrpc_utils.h"
- 
--static bool set_light_called = false;
-+static int set_light_call_count = false;
- 
- void
- lgtd_proto_set_light_from_hsbk(struct lgtd_client *client,
-@@ -55,19 +55,38 @@
-             1, "Invalid temperature: %d, expected: 4200", kelvin
-         );
-     }
--    if (transition_msecs != 60) {
--        errx(
--            1, "Invalid transition duration: %d, expected: 60", transition_msecs
--        );
-+    switch (set_light_call_count++) {
-+    case 0:
-+        if (transition_msecs != 600) {
-+            errx(
-+                1, "Invalid transition duration: %d, expected: 600",
-+                transition_msecs
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (transition_msecs != 0) {
-+            errx(
-+                1, "Invalid transition duration: %d, expected: 0",
-+                transition_msecs
-+            );
-+        }
-+        break;
-+    default:
-+        errx(1, "set_light_from_hsbk called too many times");
-     }
--    set_light_called = true;
- }
- 
- int
- main(void)
- {
-     jsmntok_t tokens[32];
--    const char json[] = ("{"
-+    int parsed;
-+    bool ok;
-+    struct lgtd_jsonrpc_request req;
-+    struct lgtd_client client = { .io = NULL, .current_request = &req };
-+
-+    const char *json = ("{"
-         "\"jsonrpc\": \"2.0\","
-         "\"method\": \"set_light_from_hsbk\","
-         "\"params\": {"
-@@ -76,27 +95,44 @@
-             "\"saturation\": 0.234, "
-             "\"brightness\": 1.0, "
-             "\"kelvin\": 4200,"
--            "\"transition\": 60"
-+            "\"transition\": 600"
-         "},"
-         "\"id\": \"42\""
-     "}");
--    int parsed = parse_json(
--        tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)
--    );
--
--    bool ok;
--    struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER;
--    struct lgtd_client client = {
--        .io = NULL, .current_request = &req, .json = json
--    };
-+    client.json = json;
-+    parsed = parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json));
-+    memset(&req, 0, sizeof(req));
-     ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-     if (!ok) {
-         errx(1, "can't parse request");
-     }
-+    lgtd_jsonrpc_check_and_call_set_light_from_hsbk(&client);
-+    if (set_light_call_count != 1) {
-+        errx(1, "lgtd_proto_set_light_from_hsbk wasn't called");
-+    }
- 
-+    // optional transition argument
-+    json = ("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"set_light_from_hsbk\","
-+        "\"params\": {"
-+            "\"target\": \"*\", "
-+            "\"hue\": 324.2341514, "
-+            "\"saturation\": 0.234, "
-+            "\"brightness\": 1.0, "
-+            "\"kelvin\": 4200"
-+        "},"
-+        "\"id\": \"42\""
-+    "}");
-+    client.json = json;
-+    parsed = parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json));
-+    memset(&req, 0, sizeof(req));
-+    ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-+    if (!ok) {
-+        errx(1, "can't parse request");
-+    }
-     lgtd_jsonrpc_check_and_call_set_light_from_hsbk(&client);
--
--    if (!set_light_called) {
-+    if (set_light_call_count != 2) {
-         errx(1, "lgtd_proto_set_light_from_hsbk wasn't called");
-     }
- 
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_from_array.c
-@@ -8,7 +8,7 @@
- 
- #include "test_jsonrpc_utils.h"
- 
--static bool set_light_called = false;
-+static int set_light_call_count = 0;
- 
- void
- lgtd_proto_set_light_from_hsbk(struct lgtd_client *client,
-@@ -55,43 +55,73 @@
-             1, "Invalid temperature: %d, expected: 4200", kelvin
-         );
-     }
--    if (transition_msecs != 0) {
--        errx(
--            1, "Invalid transition duration: %d, expected: 0", transition_msecs
--        );
-+    switch (set_light_call_count++) {
-+    case 0:
-+        if (transition_msecs != 600) {
-+            errx(
-+                1, "Invalid transition duration: %d, expected: 600",
-+                transition_msecs
-+            );
-+        }
-+        break;
-+    case 1:
-+        if (transition_msecs != 0) {
-+            errx(
-+                1, "Invalid transition duration: %d, expected: 0",
-+                transition_msecs
-+            );
-+        }
-+        break;
-+    default:
-+        errx(1, "set_light_from_hsbk called too many times");
-     }
--    set_light_called = true;
- }
- 
- int
- main(void)
- {
-     jsmntok_t tokens[32];
--    const char json[] = ("{"
-+    int parsed;
-+    bool ok;
-+    struct lgtd_jsonrpc_request req;
-+    struct lgtd_client client = {
-+        .io = NULL, .current_request = &req,
-+    };
-+
-+    const char *json = ("{"
-         "\"jsonrpc\": \"2.0\","
-         "\"method\": \"set_light_from_hsbk\","
--        "\"params\": ["
--            "\"*\", 324.2341514, 0.234, 1.0, 4200, 0"
--        "],"
-+        "\"params\": [\"*\", 324.2341514, 0.234, 1.0, 4200, 600],"
-         "\"id\": \"42\""
-     "}");
--    int parsed = parse_json(
--        tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)
--    );
--
--    bool ok;
--    struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER;
--    struct lgtd_client client = {
--        .io = NULL, .current_request = &req, .json = json
--    };
-+    client.json = json;
-+    parsed = parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json));
-+    memset(&req, 0, sizeof(req));
-     ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-     if (!ok) {
-         errx(1, "can't parse request");
-     }
-+    lgtd_jsonrpc_check_and_call_set_light_from_hsbk(&client);
-+    if (set_light_call_count != 1) {
-+        errx(1, "lgtd_proto_set_light_from_hsbk wasn't called");
-+    }
- 
-+    // optional transition
-+    json = ("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"set_light_from_hsbk\","
-+        "\"params\": [\"*\", 324.2341514, 0.234, 1.0, 4200],"
-+        "\"id\": \"42\""
-+    "}");
-+    client.json = json;
-+    parsed = parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json));
-+    memset(&req, 0, sizeof(req));
-+    ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-+    if (!ok) {
-+        errx(1, "can't parse request");
-+    }
-     lgtd_jsonrpc_check_and_call_set_light_from_hsbk(&client);
--
--    if (!set_light_called) {
-+    if (set_light_call_count != 2) {
-         errx(1, "lgtd_proto_set_light_from_hsbk wasn't called");
-     }
- 
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_light_from_hsbk_invalid_params.c
-@@ -133,5 +133,29 @@
-         "},"
-         "\"id\": \"42\""
-     "}");
-+
-+    // invalid temperature:
-+    test_request("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"set_light_from_hsbk\","
-+        "\"params\": {"
-+            "\"target\": \"*\", "
-+            "\"hue\": 324.2341514, "
-+            "\"saturation\": 0.234, "
-+            "\"brightness\": 1.0, "
-+            "\"kelvin\": -4200,"
-+            "\"transition\": 42"
-+        "},"
-+        "\"id\": \"42\""
-+    "}");
-+
-+    // too many params
-+    test_request("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"set_light_from_hsbk\","
-+        "\"params\": [\"*\", 324.2341514, 0.234, 1.0, 4200, 600, \"extraarg\"],"
-+        "\"id\": \"42\""
-+    "}");
-+
-     return 0;
- }
-diff --git a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
---- a/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
-+++ b/tests/core/jsonrpc/test_jsonrpc_check_and_call_set_waveform.c
-@@ -23,7 +23,7 @@
-     return LGTD_LIFX_WAVEFORM_SAW;
- }
- 
--static bool set_waveform_called = false;
-+static int set_waveform_call_count = 0;
- 
- void
- lgtd_proto_set_waveform(struct lgtd_client *client,
-@@ -79,23 +79,38 @@
-     if (skew_ratio != 0) {
-         errx(1, "Invalid skew_ratio: %d, expected: 0", skew_ratio);
-     }
--    if (!transient) {
--        errx(1, "transient is false instead of true");
--    }
-     if (waveform != LGTD_LIFX_WAVEFORM_SAW) {
-         errx(
-             1, "Invalid waveform %d: expected: %d",
-             waveform, LGTD_LIFX_WAVEFORM_SAW
-         );
-     }
--    set_waveform_called = true;
-+    switch (set_waveform_call_count++) {
-+    case 0:
-+        if (transient) {
-+            errx(1, "Invalid transient: true (expected false)");
-+        }
-+        break;
-+    case 1:
-+        if (!transient) {
-+            errx(1, "Invalid transient: false (expected true)");
-+        }
-+        break;
-+    default:
-+        errx(1, "set_waveform called too many times");
-+    }
- }
- 
- int
- main(void)
- {
-     jsmntok_t tokens[32];
--    const char json[] = ("{"
-+    int parsed;
-+    bool ok;
-+    struct lgtd_jsonrpc_request req;
-+    struct lgtd_client client = { .io = NULL, .current_request = &req };
-+
-+    const char *json = ("{"
-         "\"jsonrpc\": \"2.0\","
-         "\"method\": \"set_waveform\","
-         "\"params\": {"
-@@ -107,28 +122,49 @@
-             "\"cycles\": 10,"
-             "\"period\": 1000,"
-             "\"skew_ratio\": 0.5,"
--            "\"transient\": true,"
-+            "\"transient\": false,"
-             "\"waveform\": \"SAW\""
-         "},"
-         "\"id\": \"42\""
-     "}");
--    int parsed = parse_json(
--        tokens, LGTD_ARRAY_SIZE(tokens), json, sizeof(json)
--    );
--
--    bool ok;
--    struct lgtd_jsonrpc_request req = TEST_REQUEST_INITIALIZER;
--    struct lgtd_client client = {
--        .io = NULL, .current_request = &req, .json = json
--    };
-+    client.json = json;
-+    parsed = parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json));
-+    memset(&req, 0, sizeof(req));
-     ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-     if (!ok) {
-         errx(1, "can't parse request");
-     }
-+    lgtd_jsonrpc_check_and_call_set_waveform(&client);
-+    if (set_waveform_call_count != 1) {
-+        errx(1, "lgtd_proto_set_waveform wasn't called");
-+    }
- 
-+    // optional transient argument
-+    json = ("{"
-+        "\"jsonrpc\": \"2.0\","
-+        "\"method\": \"set_waveform\","
-+        "\"params\": {"
-+            "\"target\": \"*\", "
-+            "\"hue\": 324.2341514, "
-+            "\"saturation\": 0.234, "
-+            "\"brightness\": 1.0, "
-+            "\"kelvin\": 4200,"
-+            "\"cycles\": 10,"
-+            "\"period\": 1000,"
-+            "\"skew_ratio\": 0.5,"
-+            "\"waveform\": \"SAW\""
-+        "},"
-+        "\"id\": \"42\""
-+    "}");
-+    client.json = json;
-+    parsed = parse_json(tokens, LGTD_ARRAY_SIZE(tokens), json, strlen(json));
-+    memset(&req, 0, sizeof(req));
-+    ok = lgtd_jsonrpc_check_and_extract_request(&req, tokens, parsed, json);
-+    if (!ok) {
-+        errx(1, "can't parse request");
-+    }
-     lgtd_jsonrpc_check_and_call_set_waveform(&client);
--
--    if (!set_waveform_called) {
-+    if (set_waveform_call_count != 2) {
-         errx(1, "lgtd_proto_set_waveform wasn't called");
-     }
- 
-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
-@@ -40,7 +40,7 @@
-         "\"error\": {"
-             "\"code\": -32602, "
-             "\"message\": "
--            "\"Invalid number of parameters\""
-+            "\"Invalid parameters\""
-         "}"
-     "}");
- 
--- a/series	Sat May 28 18:42:04 2016 -0700
+++ b/series	Sat May 28 19:00:30 2016 -0700
@@ -1,10 +1,3 @@
-add_make_release.patch
-fix_freebsd_build.patch
-docs_setup_alabaster_and_ga.patch
-optional_jsonrpc_args.patch
-network_discovery.patch
-dont_use_ev_assign.patch
-white_colors_clarifications.patch
 add_power_transition.patch
 open_gateway_on_any_bulb_response.patch #+future
 make_gateway_write_callbacks_low_priority.patch #+future
--- a/white_colors_clarifications.patch	Sat May 28 18:42:04 2016 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-# HG changeset patch
-# Parent  6b4f3d5c20f291c5fe0f908810931bd7a92ac228
-Improve the protocol docs a bit
-
-Go beyond just saying what the parameters are and clarify whites vs
-colors in set_light_from_hsbk and set_waveform.
-
-Clarify the example client a bit and fix a typo.
-
-Closes GH-9.
-
-diff --git a/docs/protocol.rst b/docs/protocol.rst
---- a/docs/protocol.rst
-+++ b/docs/protocol.rst
-@@ -65,6 +65,11 @@
- 
- .. function:: set_light_from_hsbk(target, hue, saturation, brightness, kelvin[, transition])
- 
-+   Set the color of the given bulbs, if `hue` and `saturation` are both 0 then
-+   the bulbs will be white otherwise you'll be setting an actual color. The
-+   temperature parameter (`kelvin`) can be used to get a warmer or colder white
-+   and will also affect colors.
-+
-    :param float hue: From 0 to 360.
-    :param float saturation: From 0 to 1.
-    :param float brightness: From 0 to 1.
-@@ -74,6 +79,11 @@
- 
- .. function:: set_waveform(target, waveform, hue, saturation, brightness, kelvin, period, cycles, skew_ratio[, transient])
- 
-+   Repeatedly change the color of the given bulbs according to a periodic
-+   (mathematical) function selected with the `waveform` parameter. The `hue`,
-+   `saturation`, `brightness` and `kelvin` parameters work like in
-+   :func:`set_light_from_hsbk`.
-+
-    :param string waveform: One of ``SAW``, ``SINE``, ``HALF_SINE``,
-                            ``TRIANGLE``, ``SQUARE``.
-    :param float hue: From 0 to 360.
-@@ -88,7 +98,7 @@
-                           its original state. This argument is optional and
-                           defaults to true.
- 
--   The meaning of the ``skew_ratio`` argument depends on the type of waveform:
-+   The meaning of the `skew_ratio` argument depends on the selected `waveform`:
- 
-    +---------------+-----------------------------------------------------------+
-    | ``SAW``       | Should be 0.5.                                            |
-@@ -111,18 +121,20 @@
-    targeted bulb, the list is not in any specific order. Each dict has the
-    following fields:
- 
--   - hsbk: tuple (h, s, b, k) see function:`set_light_from_hsbk`;
--   - label: bulb label (utf-8 encoded string);
--   - power: boolean, true when the bulb is powered on false otherwise;
--   - tags: list of tags applied to the bulb (utf-8 encoded strings).
-+   - hsbk: tuple (h, s, b, k) as in :func:`set_light_from_hsbk`;
-+   - label: bulb label;
-+   - power: boolean, true when the bulb is powered on, false otherwise;
-+   - tags: list of tags applied to the bulb.
- 
- .. function:: set_label(target, label)
- 
--   Label the target bulb(s) with the given label.
-+   Label the target bulb(s) with the given label. UTF-8 encoded values are
-+   recommended.
- 
-    .. note::
- 
--      Use :func:`tag` instead set_label to give a common name to multiple bulbs.
-+      Use :func:`tag` instead of :func:`set_label` to give a common name to
-+      multiple bulbs.
- 
- .. function:: tag(target, label)
- 
-@@ -227,13 +239,16 @@
-    while True:
-        # Read a chunk of data, and accumulate it in the response buffer:
-        response += lightsd_socket.recv(READ_SIZE)
-+       # Try to load the received the data, we ignore encoding and parsing
-+       # errors since we only wanna know if the received data is complete:
-        try:
--           # Try to load the received the data, we ignore encoding errors
--           # since we only wanna know if the received data is complete.
-            json.loads(response.decode(ENCODING, "ignore"))
--           break  # Decoding was successful, we have received everything.
-+           # Decoding and JSON parsing were successful, we have received
-+           # everything:
-+           break 
-        except Exception:
--           continue  # Decoding failed, data must be missing.
-+           # Decoding or parsing failed, data must be missing, try again:
-+           continue
- 
-    response = response.decode(ENCODING, "surrogateescape")
-    print(json.loads(response))