lifxlan-go

module
v0.6.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT

README ΒΆ

lifxlan-go

lifxlan-go is a Go client library for discovering and controlling LIFX smart lights over your local network using the LIFX LAN protocol.

It provides everything needed to build local-first LIFX applications, including device discovery, protocol messaging, state tracking, and a natural-language command parser.

This library is designed to be lightweight, idiomatic, and suitable for CLI tools, desktop apps, automation services, and embedded controllers.

Features

  • Discover LIFX devices via UDP broadcast
  • Send and receive messages using the LIFX LAN protocol
  • Manage per-device sessions
  • Track device state (power, color, label, etc.)
  • Perform periodic discovery and session health checks
  • Natural-language command parsing β†’ protocol messages
  • Fully testable and modular architecture
  • Extensible for advanced control

Installation

go get github.com/alessio-palumbo/lifxlan-go

Usage

import (
	"fmt"
	"log"
	"time"

	"github.com/alessio-palumbo/lifxlan-go/pkg/controller"
)

func main() {
	ctrl, err := controller.New()
	if err != nil {
		log.Fatal(err)
	}
	defer ctrl.Close()

	time.Sleep(time.Second)

	devices := ctrl.GetDevices()
	for _, d := range devices {
		fmt.Printf("Found device: %s - %s (PoweredOn: %t)\n", d.Serial, d.Label, d.PoweredOn)
	}
}

The controller is silent by default. To receive controller and device-session logs, pass a standard log/slog logger:

logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
ctrl, err := controller.New(controller.WithLogger(logger))

Target Selection

pkg/device includes small selector helpers for apps that let users refer to devices by serial, label, group, location, or all.

devices := ctrl.GetDevices()
serials := device.ResolveSelectorSerials("desk, office", devices)

for _, serial := range serials {
	err := ctrl.Send(serial, messages.SetPowerOn())
	if err != nil {
		return err
	}
}

Selectors are comma-separated, case-insensitive exact matches. Results preserve selector order, preserve device discovery order inside each selector, and de-duplicate serials.

State Snapshot And Restore

Controllers can capture and restore the current light state for one or more devices. This is useful when an app runs a temporary scene, effect, or choreography and wants to put lights back afterwards.

ctx := context.Background()
serials := device.ResolveSelectorSerials("desk, office", ctrl.GetDevices())

snapshot, err := ctrl.CaptureStateSnapshot(ctx, serials, controller.SnapshotOptions{
	Timeout: 3 * time.Second,
})
if err != nil {
	return err
}

defer ctrl.RestoreStateSnapshot(context.Background(), snapshot, controller.RestoreOptions{
	Duration: 500 * time.Millisecond,
	Attempts: 2,
})

Snapshot capture requests the state needed for each light type: power and color for single-zone lights, zone colors for multizone lights, and matrix chain colors for matrix lights. Restore replays the matching protocol messages later.

The library handles the LIFX-specific state shape, while applications still own policy decisions such as when a snapshot is stale, how long to wait before starting an effect, and whether to retry restoration.

Color Helpers

device.Color stores hue, saturation, and brightness as user-facing percentages/degrees. Helpers are available for common brightness rules:

device.ClampBrightness(value)        // clamp to [0, 100]
device.ClampVisibleBrightness(value) // clamp to [1, 100]
device.ScaleBrightness(value, 0.5)   // scale and keep a visible brightness

Effects

The pkg/effects package generates deterministic, target-free frames that can be used live or rendered offline. Frames do not contain serials, groups, labels, or network commands; device targeting is handled by renderers.

The shared rendering pipeline is:

Effect -> Frame -> device.Surface -> DeviceFrame -> renderer/messages

device.SurfaceFromDevice derives logical/display layout and physical send metadata from a discovered device. This includes multizone sizing, matrix chain bounds, send width, row offsets, hidden cells, and matrix orientation.

Run Effects Live
import (
	"context"
	"time"

	"github.com/alessio-palumbo/lifxlan-go/pkg/controller"
	"github.com/alessio-palumbo/lifxlan-go/pkg/device"
	"github.com/alessio-palumbo/lifxlan-go/pkg/effects"
	"github.com/alessio-palumbo/lifxlan-go/pkg/effects/adapters"
	"github.com/alessio-palumbo/lifxlan-go/pkg/protocol"
)

func runSweep(ctx context.Context, ctrl *controller.Controller, dev device.Device) error {
	send := func(msg *protocol.Message) error {
		return ctrl.Send(dev.Serial, msg)
	}

	caps := effects.CapabilitiesFromDevice(dev)
	palette := effects.Palette{
		Base:        []effects.Color{{Hue: 210, Saturation: 100, Brightness: 35, Kelvin: 3500}},
		Accents:     []effects.Color{{Hue: 25, Saturation: 100, Brightness: 60, Kelvin: 3000}},
		Backgrounds: []effects.Color{{Hue: 260, Saturation: 85, Brightness: 8, Kelvin: 3500}},
	}

	return adapters.RunEffects(ctx, dev, send,
		effects.RunConfig{
			Effect: effects.NewSweep(effects.SweepConfig{
				Capabilities: caps,
				Palette:      palette,
			}),
			Duration: 10 * time.Second,
			Step:     120 * time.Millisecond,
		},
	)
}

adapters.RunEffects configures the right renderer from the discovered device:

  • single-zone lights use color messages
  • multizone lights adapt frames to the device surface and use extended zone color messages
  • matrix lights adapt frames to the device surface, preserve send width/layout, and apply device orientation when sending tile color messages

For lower-level control, build a renderer yourself:

renderer := adapters.NewRendererForDevice(dev, send)
runner := effects.NewRunner(effect, renderer, 120*time.Millisecond)
err := runner.Run(ctx)

You can also pass a known surface to lower-level renderers:

surface := device.SurfaceFromDevice(dev)
renderer := adapters.NewMatrixRenderer(send, adapters.WithMatrixSurface(surface))
Render Offline

Use effects.Render when you need timestamped frames without touching the network. This is useful for tests, previews, or offline choreography generation.

frames := effects.Render(
	effects.NewGradient(effects.GradientConfig{
		Capabilities: effects.Capabilities{Width: 8, Height: 8},
		Palette:      palette,
	}),
	100*time.Millisecond,
	10*time.Second,
)

To convert a logical frame into packet-independent device frames, adapt it to a surface:

surface := device.SurfaceFromDevice(dev)
deviceFrames, err := effects.AdaptFrameToSurface(frames[0].Frame, surface, effects.AdaptOptions{})

The resulting DeviceFrame values contain colors, duration, send width, chain index, and orientation metadata. They can be serialized into a timeline, rendered in a preview, or converted to LAN messages later.

Available effects include Solid, Gradient, GradientDrift, PaletteSweep, Comet, Sparkle, Sweep, Flow, Ring, Waterfall, Rockets, Snake, Worm, Wave, and ConcentricFrames.

Flow defaults to a moving brightness crest. For filled matrix-style color motion where palette brightness should stay constant, use:

flow := effects.NewFlow(effects.FlowConfig{
	Capabilities:   caps,
	Palette:        palette,
	Axis:           effects.FlowAxisDiagonal,
	BrightnessMode: effects.FlowBrightnessConstant,
	Sampling:       effects.FlowSamplingInterpolate,
})

Flow and GradientDrift default to whole-cell palette steps. Use FlowSamplingInterpolate when slower live effects should blend between palette stops instead of holding each zone offset until the next step.

PaletteSweep moves a multi-color band over a dim drifting gradient background. It is useful for strip and matrix choreography where the whole surface should stay lit while a stronger palette band travels across it.

The older pkg/matrix effect helpers are kept for compatibility, but new code should prefer pkg/effects plus pkg/effects/adapters. The newer API separates deterministic frame generation from live LAN rendering and also supports offline timeline generation.

πŸ› οΈ Creating Custom LIFX Messages

The messages package provides helpers to build your own LAN messages using the lifxprotocol-go types.

import (
	"github.com/alessio-palumbo/lifxlan-go/pkg/protocol"
	"github.com/alessio-palumbo/lifxprotocol-go/gen/protocol/packets"
)

var SetColor = protocol.NewMessage(&packets.LightSetColor{
	Color:    packets.LightHsbk{Hue: 65535, Saturation: 65535, Brightness: 32768, Kelvin: 3500},
	Duration: 1000,
})

Then you can send it using the controller:

err = controller.Send(deviceAddr, msg)

🌐 Multi-Network Discovery

By default, lifxlan-go keeps its existing automatic behavior and broadcasts on the first suitable IPv4 interface. For machines connected to multiple networks, applications can list broadcast-capable interfaces and let users choose one.

ifaces, err := client.BroadcastInterfaces()
if err != nil {
	panic(err)
}
for _, iface := range ifaces {
	fmt.Printf("%s %s -> %s\n", iface.Name, iface.IP, iface.Broadcast)
}

Use no option for automatic selection, or pass a selected interface when creating the controller.

ctrl, err := controller.New(controller.WithClientConfig(&client.Config{
	BroadcastInterfaceName: "en0",
}))

Advanced callers can also provide an exact broadcast address. If the port is zero, the default LIFX UDP port is used.

c, err := client.NewClient(&client.Config{
	BroadcastAddr: &net.UDPAddr{IP: net.IPv4(192, 168, 1, 255)},
})

If an application changes the selected interface at runtime, close the current controller and create a new one so discovery and device sessions are rebuilt for the selected network.

πŸ”§ Using the Client Directly

If you prefer low-level control or want to use your own device management logic, you can use the Client directly without the higher-level Controller.

This is ideal for:

  • Quick testing
  • One-off commands
  • Custom applications that don’t need device sessions or periodic discovery
import (
	"fmt"
	"net"
	"time"

	"github.com/alessio-palumbo/lifxlan-go/pkg/client"
	"github.com/alessio-palumbo/lifxlan-go/pkg/protocol"
	"github.com/alessio-palumbo/lifxprotocol-go/gen/protocol/packets"
)

func main() {
	c, err := client.NewClient(nil)
	if err != nil {
		panic(err)
	}
	defer c.Close()

	done := make(chan struct{})
	go c.Receive(2*time.Second, false, func(m *protocol.Message, addr *net.UDPAddr) {
		fmt.Printf("Received: %+v from %v\n", m.Target(), addr)
		close(done)
	})

	msg := protocol.NewMessage(&packets.DeviceGetService{})
	err = c.SendBroadcast(msg)
	if err != nil {
		panic(err)
	}

	<-done
}

You can:

  • Use client.Send() or client.SendBroadcast() to send commands.
  • Start a background client.Receive() to process incoming messages.
  • Build and customize your own logic for managing responses.

🧠 Command Parsing

The command parser converts user text into executable protocol messages. See pkg/command/README.md for the supported grammar and current limitations. This allows applications to support natural commands like:

set kitchen lights orange 50%
desk lamp off
bedroom lights blue and dim 20%

Example:

parser := commandparser.NewCommandParser(devices)
cmds := parser.Parse("kitchen lights warm white 50%")

for _, cmd := range cmds {
    cmd.ForEachSend(func(s device.Serial, msg *protocol.Message) {
		_ = ctrl.Send(s, msg)
    })
}
Matching and autocomplete

The parser also supports matching device names, groups, or locations based on partial or fuzzy input using:

matches := parser.Match("ki") // returns top matches for "ki", e.g. ["kitchen lights", "kit lamp"]

You can use Match(term) for autocomplete, suggestions, or fuzzy device selection in your UI or CLI application.

πŸ“¦ Dependencies

This package depends on:

  • (lifxprotocol-go)[github.com/alessio-palumbo/lifxprotocol-go]: provides the generated protocol structs and enums.
  • (lifxregistry-go)[github.com/alessio-palumbo/lifxregistry-go]: provides products information through the generated LIFX public registry.

Add it to your project:

go get github.com/alessio-palumbo/lifxprotocol-go
go get github.com/alessio-palumbo/lifxregistry-go

Environment Variables

LIFX_LOG_LEVEL: Set the log level (info, debug, warn, error). Default is info.

Project Structure

  • pkg/controller – high-level controller for managing sessions and device state
  • pkg/device – contains Device definition, properties, and surface/layout metadata
  • pkg/client – low-level UDP client for communicating with LIFX protocol
  • pkg/protocol – contains the LIFX Message library
  • pkg/messages – a selection of ready-to-use LIFX messages
  • pkg/effects – deterministic frame effects, live runners, and LIFX render adapters
  • pkg/matrix – legacy matrix editing and blocking effect helpers; prefer pkg/effects for new code
  • pkg/command – simple natural-language β†’ Command compiler

Contributing

Issues, feature requests, and PRs are welcome!

License

MIT

Directories ΒΆ

Path Synopsis
internal
pkg
command
Package command implements a lightweight natural-language command parser for LIFX device control.
Package command implements a lightweight natural-language command parser for LIFX device control.
effects
Package effects defines deterministic, device-neutral lighting effect types.
Package effects defines deterministic, device-neutral lighting effect types.
matrix
Package matrix contains legacy helpers for editing matrix frames and running blocking matrix effects.
Package matrix contains legacy helpers for editing matrix frames and running blocking matrix effects.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL