@bobfrankston/lxlan 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/notes.md DELETED
@@ -1,530 +0,0 @@
1
- # LIFX LAN Library Design - lxlan
2
-
3
- **Updated: 2026-01-11 by Claude Code**
4
-
5
- ## Overview
6
-
7
- TypeScript library for LIFX device control over LAN using UDP protocol. Designed for simplicity and portability.
8
-
9
- ## Architecture
10
-
11
- ```
12
- ┌─────────────────────────────────────────────────────────┐
13
- │ Application │
14
- ├─────────────────────────────────────────────────────────┤
15
- │ @bobfrankston/lxlan │
16
- │ ├── LxClient - main entry, event emitter │
17
- │ ├── LxDevice - device abstraction │
18
- │ └── LxProtocol - message encode/decode │
19
- ├─────────────────────────────────────────────────────────┤
20
- │ @bobfrankston/lxudp (separate package) │
21
- │ └── UDP transport with port sharing, retry, timeouts │
22
- │ Future: web server proxy for browser UDP access │
23
- │ May complement/replace y:\dev\homecontrol\utils\netsupport │
24
- ├─────────────────────────────────────────────────────────┤
25
- │ @bobfrankston/colorlib (separate package) │
26
- │ └── Color conversions: RGB ↔ HSL ↔ HSBK ↔ Kelvin │
27
- └─────────────────────────────────────────────────────────┘
28
- ```
29
-
30
- ## Design Decisions
31
-
32
- ### Async Model (Fire-and-Forget)
33
- - **Send methods return when packet sent**, not when response received
34
- - **Results delivered via EventEmitter**
35
- - Sequence numbers track request/response internally
36
-
37
- ### Event Emission Policy
38
- - **Emit on every message** from device, even if state unchanged
39
- - **Cache state** on device object for sync queries
40
- - Traffic is light - no throttling needed
41
-
42
- ### Retry Policy
43
- - **Auto-retry** with configurable count (default 3)
44
- - **Configurable via API** at runtime, not just constructor
45
- - **Long timeouts supported** - up to 10+ seconds for network issues
46
- - Settings: retryCount, retryDelay, responseTimeout
47
-
48
- ### Port Sharing
49
- - Uses `SO_REUSEADDR` via dgram `reuseAddr: true`
50
- - Multiple instances share port 56700
51
-
52
- ### Scope - v1
53
- - **No multizone** - treat as single device (see Future section)
54
- - **Include label/group/location get/set** - for naming management
55
- - Strategy: set device labels to factory-default-based names, use external DB for management
56
-
57
- ### Separation of Concerns
58
- - **lxcolor**: Pure color math, no dependencies
59
- - **lxudp**: Generic UDP transport, reusable
60
- - **lxlan**: LIFX protocol only, uses lxudp
61
-
62
- ---
63
-
64
- ## lxudp API (separate package)
65
-
66
- Reusable UDP transport with port sharing, retry logic, configurable timeouts.
67
-
68
- ```typescript
69
- import { EventEmitter } from 'events';
70
-
71
- interface UdpSocketOptions {
72
- port?: number; /** default 0 (ephemeral) */
73
- reuseAddr?: boolean; /** default true */
74
- broadcastAddr?: string; /** default "255.255.255.255" */
75
- }
76
-
77
- interface UdpRetryOptions {
78
- retryCount?: number; /** default 3 */
79
- retryDelay?: number; /** ms between retries, default 100 */
80
- responseTimeout?: number; /** ms to wait for response, default 1000 */
81
- }
82
-
83
- class UdpSocket extends EventEmitter {
84
- constructor(options?: UdpSocketOptions);
85
-
86
- // Lifecycle
87
- bind(): Promise<void>;
88
- close(): void;
89
-
90
- // Runtime config
91
- setRetryOptions(options: UdpRetryOptions): void;
92
- getRetryOptions(): UdpRetryOptions;
93
-
94
- // Send
95
- send(ip: string, port: number, data: Buffer): void; /** fire-and-forget */
96
- broadcast(data: Buffer, port?: number): void;
97
-
98
- // Events:
99
- // 'message' (data: Buffer, rinfo: { address: string, port: number })
100
- // 'error' (err: Error)
101
- // 'bound' ()
102
- }
103
-
104
- // Future: UdpProxy for web server bridge
105
- // class UdpProxyServer { ... }
106
- ```
107
-
108
- ---
109
-
110
- ## colorlib API (separate package)
111
-
112
- Generic color conversion library. No LIFX specifics.
113
-
114
- ```typescript
115
- // Types
116
- interface RGB { r: number; g: number; b: number; } /** 0-255 */
117
- interface HSL { h: number; s: number; l: number; } /** h: 0-360, s/l: 0-100 */
118
- interface HSB { h: number; s: number; b: number; } /** h: 0-360, s/b: 0-100 */
119
- interface HSBK { h: number; s: number; b: number; k: number; } /** k: 1500-9000 Kelvin */
120
-
121
- // Conversions
122
- function rgbToHsl(rgb: RGB): HSL;
123
- function hslToRgb(hsl: HSL): RGB;
124
- function rgbToHsb(rgb: RGB): HSB;
125
- function hsbToRgb(hsb: HSB): RGB;
126
- function hsbToHsbk(hsb: HSB, kelvin?: number): HSBK; /** default 3500K */
127
- function kelvinToRgb(kelvin: number): RGB; /** approximate white point */
128
-
129
- // Parsing - flexible input
130
- function parseColor(input: string | RGB | HSL | HSB | HSBK | number): HSBK;
131
- // Accepts: "#ff0000", "rgb(255,0,0)", "red", {r,g,b}, {h,s,l}, {h,s,b,k}, 6500 (kelvin)
132
-
133
- // 16-bit wire format conversion
134
- function hsbkTo16(hsbk: HSBK): HSBK16; /** h/s/b: 0-0xFFFF, k: 1500-9000 */
135
- function hsbk16ToHsbk(hsbk16: HSBK16): HSBK;
136
- ```
137
-
138
- ---
139
-
140
- ## lxlan API
141
-
142
- ### LxClient - Main Entry Point
143
-
144
- ```typescript
145
- import { EventEmitter } from 'events';
146
-
147
- interface LxClientOptions {
148
- port?: number; /** default 56700 */
149
- broadcastAddr?: string; /** default "255.255.255.255" */
150
- discoveryInterval?: number; /** ms, 0 = manual only */
151
- }
152
-
153
- class LxClient extends EventEmitter {
154
- constructor(options?: LxClientOptions);
155
-
156
- // Lifecycle
157
- start(): void;
158
- stop(): void;
159
-
160
- // Runtime configuration
161
- setRetryOptions(options: { retryCount?: number; retryDelay?: number; responseTimeout?: number }): void;
162
- getRetryOptions(): { retryCount: number; retryDelay: number; responseTimeout: number };
163
-
164
- // Discovery
165
- discover(): void;
166
-
167
- // Device access (sync - reads cache)
168
- devices: Map<string, LxDevice>;
169
- getDevice(mac: string): LxDevice;
170
- addDevice(mac: string, ip: string, port?: number): LxDevice; /** manual registration */
171
-
172
- // Events:
173
- // 'device' (device: LxDevice) - new device discovered
174
- // 'message' (device: LxDevice, msg: LxMessage) - any message from device
175
- // 'state' (device: LxDevice) - state message (always emits)
176
- // 'power' (device: LxDevice) - power message
177
- // 'label' (device: LxDevice) - label changed
178
- // 'group' (device: LxDevice) - group info received
179
- // 'location' (device: LxDevice) - location info received
180
- // 'online' (device: LxDevice) - device online
181
- // 'offline' (device: LxDevice) - device offline
182
- // 'error' (error: Error) - transport/protocol error
183
- }
184
- ```
185
-
186
- ### LxDevice - Device Abstraction
187
-
188
- ```typescript
189
- interface LxDevice {
190
- // Identity
191
- mac: string;
192
- ip: string;
193
- port: number;
194
- label: string;
195
-
196
- // Cached state
197
- power: boolean;
198
- color: HSBK;
199
- online: boolean;
200
- lastSeen: number;
201
-
202
- // Product info
203
- vendor: number;
204
- product: number;
205
- productName: string;
206
-
207
- // Commands - fire-and-forget with auto-retry
208
- setPower(on: boolean): void;
209
- setColor(color: string | RGB | HSL | HSB | HSBK | number, duration?: number): void;
210
- setWhite(kelvin: number, brightness?: number, duration?: number): void;
211
-
212
- // Queries - trigger fetch, results via events
213
- getState(): void;
214
- getPower(): void;
215
-
216
- // Label/Group/Location (supported but not fundamental)
217
- setLabel(label: string): void;
218
- getLabel(): void;
219
- setGroup(id: string, label: string): void;
220
- getGroup(): void;
221
- setLocation(id: string, label: string): void;
222
- getLocation(): void;
223
-
224
- // Cached group/location info
225
- group: { id: string; label: string; updatedAt: number };
226
- location: { id: string; label: string; updatedAt: number };
227
-
228
- // Raw
229
- send(type: number, payload?: Buffer): void;
230
- }
231
- ```
232
-
233
- ### LxProtocol - Message Encoding
234
-
235
- ```typescript
236
- enum LxMessageType {
237
- GetService = 2,
238
- StateService = 3,
239
- GetPower = 20,
240
- SetPower = 21,
241
- StatePower = 22,
242
- GetLabel = 23,
243
- SetLabel = 24,
244
- StateLabel = 25,
245
- GetVersion = 32,
246
- StateVersion = 33,
247
- GetLocation = 48,
248
- SetLocation = 49,
249
- StateLocation = 50,
250
- GetGroup = 51,
251
- SetGroup = 52,
252
- StateGroup = 53,
253
- Get = 101,
254
- SetColor = 102,
255
- State = 107,
256
- }
257
-
258
- interface LxMessage {
259
- size: number;
260
- tagged: boolean;
261
- source: number;
262
- target: string;
263
- sequence: number;
264
- type: LxMessageType;
265
- payload: Buffer;
266
- }
267
-
268
- function encodeMessage(msg: Partial<LxMessage>): Buffer;
269
- function decodeMessage(data: Buffer): LxMessage;
270
- ```
271
-
272
- ---
273
-
274
- ## Usage Examples
275
-
276
- ### Basic Discovery and Control
277
-
278
- ```typescript
279
- import { LxClient } from '@bobfrankston/lxlan';
280
-
281
- const client = new LxClient();
282
-
283
- client.on('device', (device) => {
284
- console.log(`Found: ${device.label} (${device.mac})`);
285
- });
286
-
287
- client.on('state', (device) => {
288
- console.log(`${device.label}: power=${device.power}`);
289
- });
290
-
291
- client.start();
292
- client.discover();
293
-
294
- // Adjust for slow network
295
- client.setRetryOptions({ retryCount: 5, responseTimeout: 10000 });
296
-
297
- // Control
298
- const bulb = client.getDevice('d0:73:d5:xx:xx:xx');
299
- bulb.setPower(true);
300
- bulb.setColor('#ff0000', 1000);
301
- bulb.setWhite(2700, 80);
302
- ```
303
-
304
- ---
305
-
306
- ## LIFX Protocol Summary
307
-
308
- ### Header (36 bytes)
309
- - Frame: size, protocol, tagged, source
310
- - Address: target MAC, sequence
311
- - Protocol: message type
312
-
313
- ### Color (HSBK) - 8 bytes
314
- | Field | Bits | Range | Maps to |
315
- |-------|------|-------|---------|
316
- | Hue | 16 | 0-65535 | 0-360° |
317
- | Saturation | 16 | 0-65535 | 0-100% |
318
- | Brightness | 16 | 0-65535 | 0-100% |
319
- | Kelvin | 16 | 1500-9000 | temp |
320
-
321
- ---
322
-
323
- ## File Structure
324
-
325
- ```
326
- lxudp/
327
- ├── index.ts
328
- ├── socket.ts - UdpSocket class
329
- ├── address.ts - getBroadcastAddresses(), computeBroadcast()
330
- ├── types.ts
331
- ├── package.json
332
- └── tsconfig.json
333
-
334
- colorlib/
335
- ├── index.ts
336
- ├── convert.ts
337
- ├── parse.ts
338
- ├── types.ts
339
- ├── package.json
340
- └── tsconfig.json
341
-
342
- lxlan/
343
- ├── index.ts
344
- ├── client.ts
345
- ├── device.ts
346
- ├── protocol.ts
347
- ├── types.ts
348
- ├── package.json
349
- └── tsconfig.json
350
- ```
351
-
352
- ## Implementation Order
353
-
354
- 1. **lxcolor** - pure color math
355
- 2. **lxudp** - UDP transport with retry
356
- 3. **lxlan/types.ts, protocol.ts** - message handling
357
- 4. **lxlan/device.ts, client.ts** - device abstraction
358
- 5. **tests/** - discovery, control
359
-
360
- ---
361
-
362
- ## Future - v2 Considerations
363
-
364
- ### Multizone Support (LIFX Beam, Z strips)
365
- Multizone devices have individually addressable LED segments.
366
-
367
- **Additional message types:**
368
- | Type | Name | Description |
369
- |------|------|-------------|
370
- | 501 | SetColorZones | Set color for zone range |
371
- | 502 | GetColorZones | Query zone colors |
372
- | 503 | StateZone | Single zone response |
373
- | 506 | StateMultiZone | Multiple zones response |
374
-
375
- **API additions for LxDevice:**
376
- ```typescript
377
- interface LxDevice {
378
- // Multizone properties
379
- zoneCount: number; /** 0 = not multizone */
380
- zones: HSBK[]; /** cached zone colors */
381
-
382
- // Multizone commands
383
- setZoneColor(start: number, end: number, color: HSBK, duration?: number): void;
384
- getZones(start?: number, end?: number): void;
385
- }
386
- ```
387
-
388
- **Events:**
389
- - `'zones'` (device: LxDevice) - zone state updated
390
-
391
- ### UDP Web Proxy (lxudp-server)
392
- HTTP/WebSocket server exposing UDP operations for browsers.
393
- ```typescript
394
- // Future API sketch
395
- const proxy = new UdpProxyServer({ httpPort: 8080 });
396
- proxy.start();
397
- // Browser: ws://localhost:8080/udp
398
- ```
399
-
400
- ### Related: y:\dev\homecontrol\utils\netsupport
401
- Evaluate overlap with existing netsupport utilities. lxudp may complement or replace portions.
402
-
403
- ---
404
-
405
- ## Separate Future Project: lxhttp
406
-
407
- **Entirely separate library** - LIFX Cloud HTTP API. Not part of lxlan.
408
-
409
- ### Purpose
410
- - Enumerate devices including **offline** ones (cloud knows all registered devices)
411
- - Alternative control path when LAN unavailable
412
- - Account-level operations
413
-
414
- ### Key Differences from lxlan
415
- | Aspect | lxlan (UDP) | lxhttp (Cloud) |
416
- |--------|-------------|----------------|
417
- | Offline devices | No | Yes |
418
- | Latency | ~ms | ~100ms+ |
419
- | Rate limits | None | Yes (cloud) |
420
- | Auth | None | OAuth token |
421
- | Local network | Required | Not required |
422
-
423
- ### API Sketch (future)
424
- ```typescript
425
- class LxHttpClient extends EventEmitter {
426
- constructor(token: string);
427
-
428
- listDevices(): void; /** includes offline */
429
- getDevice(id: string): void;
430
- setPower(id: string, on: boolean): void;
431
- setColor(id: string, color: HSBK, duration?: number): void;
432
- // ... similar to lxlan but via HTTP
433
- }
434
- ```
435
-
436
- ### lxmanager (future app)
437
- Application using **both** lxlan and lxhttp:
438
- - lxhttp for full device enumeration (including offline)
439
- - lxlan for fast local control
440
- - External database for device/group management
441
- - Device labels set to factory-default-based names
442
-
443
- ---
444
-
445
- ## UDP Proxy for Android/Browser
446
-
447
- Android and browsers cannot send UDP directly. Need a local proxy server.
448
-
449
- ### Architecture
450
- ```
451
- ┌──────────────┐ HTTP/WS ┌──────────────┐ UDP ┌──────────┐
452
- │ Android App │ ◄──────────────► │ UDP Proxy │ ◄────────────► │ LIFX │
453
- │ or Browser │ │ (Node.js) │ │ Bulbs │
454
- └──────────────┘ └──────────────┘ └──────────┘
455
- ```
456
-
457
- ### Proxy REST API (suggested)
458
-
459
- **Discovery**
460
- ```
461
- POST /discover
462
- → triggers broadcast, returns immediately
463
-
464
- GET /devices
465
- → returns cached device list
466
-
467
- GET /devices/:mac
468
- → returns single device state
469
- ```
470
-
471
- **Control**
472
- ```
473
- POST /devices/:mac/power
474
- Body: { "on": true }
475
-
476
- POST /devices/:mac/color
477
- Body: { "color": "#ff0000", "duration": 1000 }
478
- Body: { "color": { "h": 120, "s": 100, "b": 100 }, "duration": 500 }
479
- Body: { "kelvin": 2700, "brightness": 80 }
480
-
481
- POST /devices/:mac/label
482
- Body: { "label": "Kitchen Light" }
483
- ```
484
-
485
- **Events (WebSocket)**
486
- ```
487
- WS /events
488
- → receives: { "event": "state", "mac": "d0:73:...", "device": {...} }
489
- → receives: { "event": "device", "mac": "d0:73:...", "device": {...} }
490
- → receives: { "event": "offline", "mac": "d0:73:..." }
491
- ```
492
-
493
- ### Implementation Notes
494
- - Proxy uses lxlan internally
495
- - Stateless HTTP for commands
496
- - WebSocket for real-time events
497
- - Single proxy instance per LAN
498
- - Can run on Raspberry Pi, NAS, or any Node.js host
499
-
500
- ---
501
-
502
- ## LIFX Cloud HTTP API Reference
503
-
504
- Official API: https://api.lifx.com/
505
-
506
- ### Authentication
507
- ```
508
- Authorization: Bearer <token>
509
- ```
510
- Get token from https://cloud.lifx.com/settings
511
-
512
- ### Key Endpoints
513
- ```
514
- GET /v1/lights/all → list all devices
515
- GET /v1/lights/:selector → get device(s) state
516
- PUT /v1/lights/:selector/state → set power/color/brightness
517
- POST /v1/lights/:selector/toggle → toggle power
518
- POST /v1/lights/:selector/effects/... → effects (breathe, pulse, etc.)
519
- ```
520
-
521
- ### Selector Format
522
- - `all` - all devices
523
- - `id:d073d5xxxxxx` - by device ID
524
- - `label:Kitchen` - by label
525
- - `group:Living Room` - by group
526
- - `location:Home` - by location
527
-
528
- ### Rate Limits
529
- - 120 requests per 60 seconds per token
530
- - Responses include `X-RateLimit-*` headers