npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lifxlan

v0.0.84

Published

TypeScript library for controlling LIFX products over LAN

Readme

lifxlan

A fast, lightweight TypeScript library for controlling LIFX smart lights over your local network (LAN). Works with Node.js, Bun, and Deno with zero dependencies.

What does this do?

This library lets you discover and control LIFX smart lights on your local network. You can:

  • 🔍 Discover devices on your network
  • 💡 Control lights (turn on/off, change colors, brightness)
  • 🎯 Target specific devices or broadcast to all devices
  • 🔗 Group devices for batch operations
  • High performance - optimized for speed
  • 🚀 Zero dependencies - bring your own UDP socket
  • 🎛️ Direct packet control - each client operation sends exactly one packet with no hidden behavior

Quick Start

Installation

npm install lifxlan

Turn a light on (simplest example)

import dgram from 'node:dgram';
import { Client, Devices, Router, GetServiceCommand, SetPowerCommand } from 'lifxlan/index.js';

const socket = dgram.createSocket('udp4');

// Set up the router to send messages
const router = Router({
  onSend(message, port, address) {
    socket.send(message, port, address);
  },
});

// Track discovered devices
const devices = Devices();

// Handle incoming messages
socket.on('message', (message, remote) => {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.address, header.target);
});

// Start the socket
await new Promise((resolve, reject) => {
  socket.once('error', reject);
  socket.once('listening', resolve);
  socket.bind();
});

socket.setBroadcast(true);

const client = Client({ router });

// Discover devices
client.broadcast(GetServiceCommand());
const scanInterval = setInterval(() => {
  client.broadcast(GetServiceCommand());
}, 1000);

// Wait for a specific device (replace with your device's serial number)
const device = await devices.get('d07123456789');

// Stop scanning
clearInterval(scanInterval);

// Turn the light on!
await client.send(SetPowerCommand(true), device);

socket.close();

Discover and control all devices

import { GetServiceCommand, SetPowerCommand } from 'lifxlan/index.js';

// ... setup code from above ...

// Discover all devices
client.broadcast(GetServiceCommand());
const scanInterval = setInterval(() => {
  client.broadcast(GetServiceCommand());
}, 1000);

// Wait a few seconds for discovery
await new Promise(resolve => setTimeout(resolve, 3000));

// Stop scanning
clearInterval(scanInterval);

// Turn on all discovered lights
for (const device of devices) {
  await client.send(SetPowerCommand(true), device);
}

Change light color

import { SetColorCommand } from 'lifxlan/index.js';

// Set to bright red
await client.send(
  SetColorCommand(0, 65535, 65535, 3500, 0), // hue, saturation, brightness, kelvin, duration
  device
);

// Set to blue with 2-second transition
await client.send(
  SetColorCommand(43690, 65535, 65535, 3500, 2000),
  device
);

Core Concepts

Architecture Overview

The library uses three main components:

  1. Router - Handles message routing and correlation between requests/responses
  2. Client - High-level interface for sending commands with timeouts and retries
  3. Devices - Registry that tracks discovered LIFX devices on your network

Bring Your Own Socket

This library doesn't include UDP socket implementation - you provide it. This makes it work across different server-side JavaScript runtimes:

  • Node.js/Bun: Use dgram.createSocket('udp4')
  • Deno: Use Deno.listenDatagram()

Response Mode Control

The client.send() method supports flexible response modes with full type safety - the return type changes based on the response mode you choose:

// Use command defaults (recommended)
const color = await client.send(GetColorCommand(), device);     // Promise<LightState>
await client.send(SetPowerCommand(true), device);              // Promise<StatePower> (ack-only default)

// Override response behavior with type-safe returns
await client.send(command, device, { responseMode: 'ack-only' });  // Promise<void>
const data = await client.send(command, device, { responseMode: 'response' }); // Promise<T>
const result = await client.send(command, device, { responseMode: 'both' });   // Promise<T>

// With abort signal
const response = await client.send(GetColorCommand(), device, { 
  responseMode: 'both',     // TypeScript knows this returns Promise<LightState>
  signal: abortController.signal 
});
console.log(response.hue);    // ✅ TypeScript knows response is LightState

Response Modes:

  • 'auto' - Use the command's default behavior (recommended) → Promise<T>
  • 'ack-only' - Wait for acknowledgment packet (confirms receipt) → Promise<void>
  • 'response' - Wait for response data packet (Get commands) → Promise<T>
  • 'both' - Wait for both ack and response (maximum reliability) → Promise<T>

Command Defaults:

  • Get commands (GetColor, GetPower, etc.) default to 'response'
  • Set commands (SetColor, SetPower, etc.) default to 'ack-only'

Fire-and-forget: Use client.unicast() for commands that don't need confirmation

Type Safety: The return type automatically changes based on your response mode choice - no type assertions needed!

Examples by Runtime

Node.js / Bun

import dgram from 'node:dgram';
import { Client, Router, Devices, GetServiceCommand } from 'lifxlan/index.js';

const socket = dgram.createSocket('udp4');

// Router handles outgoing messages and forwards responses to clients
const router = Router({
  onSend(message, port, address) {
    // A message is ready to be sent
    socket.send(message, port, address);
  },
});

// Devices keeps track of devices discovered on the network
const devices = Devices({
  onAdded(device) {
    // A device has been discovered
    console.log(device);
  },
});

socket.on('message', (message, remote) => {
  // Forward received messages to the router
  const { header, serialNumber } = router.receive(message);
  // Forward the message to devices so it can keep track
  devices.register(serialNumber, remote.port, remote.address, header.target);
});

// Client handles communication with devices
const client = Client({ router });

socket.once('listening', () => {
  socket.setBroadcast(true);
  // Discover devices on the network
  client.broadcast(GetServiceCommand());
});

socket.bind();

setTimeout(() => {
  socket.close();
}, 1000);

Deno

import { Client, Router, Devices, GetServiceCommand } from 'lifxlan/index.js';

const socket = Deno.listenDatagram({
  hostname: '0.0.0.0',
  port: 0,
  transport: 'udp',
});

const router = Router({
  onSend(message, port, hostname) {
    socket.send(message, { port, hostname });
  }
});

const devices = Devices({
  onAdded(device) {
    console.log(device);
  },
});

const client = Client({ router });

client.broadcast(GetServiceCommand());

setTimeout(() => {
  socket.close();
}, 1000);

for await (const [message, remote] of socket) {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.hostname, header.target);
}

Common Patterns

Error Handling with Retries

for (let i = 0; i < 3; i++) {
  try {
    console.log(await client.send(GetColorCommand(), device));
    break;
  } catch (err) {
    const delay = Math.random() * Math.min(Math.pow(2, i) * 1000, 30 * 1000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

Custom Timeouts

const controller = new AbortController();

const timeout = setTimeout(() => {
  controller.abort();
}, 100);

try {
  console.log(await client.send(GetColorCommand(), device, { signal: controller.signal }));
} finally {
  clearTimeout(timeout)
}

Use Without Device Discovery

import { Client, Device, Router, SetPowerCommand } from 'lifxlan/index.js';

// ... socket setup ...

const client = Client({ router });

// Create the device directly
const device = Device({
  serialNumber: 'd07123456789',
  address: '192.168.1.50',
});

await client.send(SetPowerCommand(true), device);

Multiple Clients

const client1 = Client({ router });
const client2 = Client({ router });

// Both clients share the same router and can operate independently
await client1.broadcast(GetServiceCommand());
await client2.send(SetPowerCommand(true), device);

Resource Management for Many Clients

while (true) {
  const client = Client({ router });

  console.log(await client.send(GetPowerCommand(), device));

  // When creating a lot of clients, call dispose to avoid running out of source values
  client.dispose();
}

Response Mode Control Examples

// High-reliability mode: wait for both ack and response (typed return)
const state = await client.send(SetColorCommand(120, 100, 100, 3500, 1000), device, { 
  responseMode: 'both'     // TypeScript knows this returns Promise<LightState>
});
console.log('Confirmed color:', state.hue); // ✅ Fully typed

// Fast mode: fire-and-forget for animations (no promise)
for (let i = 0; i < 360; i += 10) {
  client.unicast(SetColorCommand(i * 182, 65535, 65535, 3500, 100), device);
  await new Promise(resolve => setTimeout(resolve, 50));
}

// Confirmation only (void return)
await client.send(SetColorCommand(120, 100, 100, 3500, 0), device, { 
  responseMode: 'ack-only' // TypeScript knows this returns Promise<void>
});

// Get response data (typed return)
const currentState = await client.send(SetColorCommand(120, 100, 100, 3500, 0), device, { 
  responseMode: 'response' // TypeScript knows this returns Promise<LightState>
});
console.log('Light is now:', currentState.hue); // ✅ Fully typed, no assertions needed

Advanced Examples

Device Groups

import { Groups, GetGroupCommand } from 'lifxlan/index.js';

const groups = Groups({
  onAdded(group) {
    console.log('Group added', group);
  },
  onChanged(group) {
    console.log('Group changed', group);
  },
});

const devices = Devices({
  async onAdded(device) {
    const group = await client.send(GetGroupCommand(), device);
    groups.register(device, group);
  },
});

// Send command to all devices in a group
for (const group of groups) {
  await Promise.all(
    group.devices.map(device => 
      client.send(GetLabelCommand(), device)
    )
  );
}

Party Mode (Animated Colors)

const PARTY_COLORS = [
  [48241, 65535, 65535, 3500], // Red
  [43690, 49151, 65535, 3500], // Blue  
  [54612, 65535, 65535, 3500], // Green
  [43690, 65535, 65535, 3500], // Cyan
  [38956, 55704, 65535, 3500], // Purple
];

while (true) {
  for (const device of devices) {
    const [hue, saturation, brightness, kelvin] = 
      PARTY_COLORS[Math.floor(Math.random() * PARTY_COLORS.length)];
    
    client.unicast(
      SetColorCommand(hue, saturation, brightness, kelvin, 1000), 
      device
    );
    
    await new Promise(resolve => setTimeout(resolve, 100));
  }
}

Custom Commands

/**
 * @param {Uint8Array} bytes
 * @param {{ current: number; }} offsetRef
 */
function decodeCustom(bytes, offsetRef) {
  const val1 = bytes[offsetRef.current++];
  const val2 = bytes[offsetRef.current++];
  return { val1, val2 };
}

function CustomCommand() {
  return {
    type: 1234,
    decode: decodeCustom,
  };
}

const res = await client.send(CustomCommand(), device);
console.log(res.val1, res.val2);

Separate Sockets for Broadcast/Unicast

const broadcastSocket = dgram.createSocket('udp4');
const unicastSocket = dgram.createSocket('udp4');

const router = Router({
  onSend(message, port, address, serialNumber) {
    if (!serialNumber) {
      broadcastSocket.send(message, port, address);
    } else {
      unicastSocket.send(message, port, address);
    }
  },
});

// ... handle messages from both sockets ...

Message Callbacks

// Router-level message callback (all messages)
const router = Router({
  onMessage(header, payload, serialNumber) {
    console.log('Router received:', header.type);
  },
});

// Client-level message callback (messages for this client)
const client = Client({
  router,
  onMessage(header, payload, serialNumber) {
    console.log('Client received:', header.type);
  },
});

Contributing

This library follows a modular architecture with clear separation between protocol, transport, and application layers. See the source code for implementation details.

License

MIT © Justin Moser