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