lifxlan
Version:
TypeScript library for controlling LIFX products over LAN
487 lines (369 loc) • 12.6 kB
Markdown
# 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
```bash
npm install lifxlan
```
### Turn a light on (simplest example)
```javascript
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
```javascript
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
```javascript
import { SetColorCommand } from 'lifxlan';
// Set to bright red
await client.send(
SetColorCommand(0, 65535, 65535, 3500), // hue, saturation, brightness, kelvin
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:
```javascript
// 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
```javascript
import dgram from 'node:dgram';
import { Client, Router, Devices, GetServiceCommand } from 'lifxlan';
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
```javascript
import { Client, Router, Devices, GetServiceCommand } from 'lifxlan';
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
```javascript
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
```javascript
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
```javascript
import { Client, Device, Router, SetPowerCommand } from 'lifxlan';
// ... 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
```javascript
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
```javascript
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
```javascript
// 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
```javascript
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)
```javascript
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
```javascript
/**
* @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
```javascript
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
```javascript
// 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