npm.io
0.1.12 • Published 2d ago

@bobfrankston/wzlan

Licence
MIT
Version
0.1.12
Deps
3
Size
38 kB
Vulns
0
Weekly
0

@bobfrankston/wzlan

Platform-agnostic Wiz LAN protocol library for controlling Wiz smart bulbs via UDP.

Architecture

wzlan (protocol core, platform-agnostic)
├── wzlan-node (Node.js adapter — NodeUdpTransport / dgram)
└── wzlan-browser (Browser adapter — WebSocket via httpudp-client)

Wiz uses JSON-over-UDP on two ports:

  • 38899 — Send commands, receive responses
  • 38900 — Receive heartbeat/syncPilot status (after registration)

Quick Start (Node.js)

import { createClient } from '@bobfrankston/wzlan-node';

const client = createClient();

client.on('device', (device) => {
    console.log(`Found: ${device.mac} ${device.ip}`);
});

client.on('state', (device) => {
    console.log(`${device.mac} power=${device.power} dim=${device.dimming} temp=${device.temp}`);
});

await client.start();   // Binds sockets, starts registration loop
// Heartbeats arrive automatically (~1/sec per bulb for ~20s, re-registered every 15s)

API

WzClient

Created via createClient() (wzlan-node) or createWzlanClient() (wzlan-browser).

Method Description
start() Bind transport, start registration loop
stop() Close sockets, stop timers
register(heartbeats?) Broadcast registration (true=heartbeats, false=discovery only)
discover() Shorthand for register(false)
getDevice(mac) Look up device by MAC
addDevice(mac, ip?, port?) Add device manually
devices Map<string, WzDevice> of all known devices
Events
Event Args Description
device (device) New device discovered
state (device) State updated (syncPilot heartbeat or setPilot response)
pilot (device, msg) getPilot response received
systemConfig (device, msg) getSystemConfig response received
message (device, msg) Any message received
error (err) Transport or protocol error
WzDevice

Extends DeviceBase from @bobfrankston/devdefs — shared state (mac, ip, port, power, label, online, lastSeen, markSeen()) and transport plumbing. See devdefs README for base class details and MAC utilities (mac12, macmac).

Properties updated automatically from heartbeats:

Property Type Description
mac string MAC address (lowercase, colon-separated)
ip string Current IP address
power boolean On/off state
dimming number Brightness 0-100
color WizColor { r, g, b, c, w } channels (0-255)
temp number Color temperature in Kelvin
sceneId number Active scene (0 = none)
speed number Animation speed
rssi number WiFi signal strength (dBm)
fwVersion string Firmware version
online boolean Responding to messages
lastSeen number Timestamp of last message
Control Methods (fire-and-forget)
device.setPower(true);              // Turn on
device.setPower(false);             // Turn off
device.setBrightness(75);            // Set brightness 0-100
device.setColor(255, 0, 0);        // Set RGB
device.setColor(0, 255, 0, 50);    // Set RGB with dimming
device.setWhite(4200);             // Set color temperature
device.setWhite(2700, 80);         // Set temp with dimming
device.setScene(4);                // Set scene by ID
device.setScene(5, 100, 80);       // Scene with speed and dimming
device.pulse(-30, 900);            // Pulse brightness
device.getPilot();                 // Query current state
device.getSystemConfig();          // Query system config
Protocol Functions

For building custom messages:

import { encodeMessage, decodeMessage, createSetPilot, createRegistration } from '@bobfrankston/wzlan';

const msg = createSetPilot({ state: true, dimming: 75 });
const bytes = encodeMessage(msg);
// Send bytes via UDP to port 38899

const response = decodeMessage(receivedBytes);
Scenes
ID Name ID Name
1 Ocean 17 True Colors
2 Romance 18 TV Time
3 Sunset 19 Plant Growth
4 Party 20 Spring
5 Fireplace 21 Summer
6 Cozy 22 Fall
7 Forest 23 Deep Dive
8 Pastel Colors 24 Jungle
9 Wake Up 25 Mojito
10 Bedtime 26 Club
11 Warm White 27 Christmas
12 Daylight 28 Halloween
13 Cool White 29 Candlelight
14 Night Light 30 Golden White
15 Focus 31 Pulse
16 Relax 32 Steampunk
1000 Rhythm
Constants
import { WIZ_PORT, WIZ_LISTEN_PORT, WizMethod, WizScenes } from '@bobfrankston/wzlan';

WIZ_PORT          // 38899 — command port
WIZ_LISTEN_PORT   // 38900 — heartbeat port
WizMethod.SetPilot      // 'setPilot'
WizMethod.GetPilot      // 'getPilot'
WizMethod.SyncPilot     // 'syncPilot'
WizMethod.Registration  // 'registration'
WizScenes[12]           // 'Daylight'

wzlan-node

Passes NodeUdpTransport as the transport factory. WzClient itself creates both sockets: ephemeral port for commands (→38899), port 38900 for heartbeats, merged into a single message handler.

import { createClient } from '@bobfrankston/wzlan-node';

const client = createClient({
    debug: true,               // Log messages to console
    discoveryInterval: 15000,  // Re-register every 15s (default)
    homeId: 704603,            // Optional home ID
});

wzlan-browser

WebSocket transport via httpudp proxy. The httpudp server handles dual-port proxying server-side.

import { createWzlanClient } from '@bobfrankston/wzlan-browser';

const client = await createWzlanClient({
    httpudpUrl: 'ws://localhost:9321',
    registrationInterval: 15000,
});

wztest CLI

Simple test/control tool:

wztest                          Interactive mode
wztest list                     List discovered bulbs
wztest <mac> -on [dim]          Turn on
wztest <mac> -off               Turn off
wztest <mac> -dim 75            Set dimming
wztest <mac> -color 255 0 0     Set RGB
wztest <mac> -white 4200        Set white temp
wztest <mac> -scene 4           Set scene (Party)
wztest -v                       Verbose (show heartbeats)

Device can be partial MAC, partial IP, or #N from list.

Wiz Protocol Reference

Local vs Cloud Architecture

Wiz bulbs expose a local UDP API on the LAN and maintain a separate cloud connection via MQTT over TLS. The local API is intentionally limited — advanced configuration is gated behind the cloud path.

Feature Local UDP Cloud (MQTT/App)
On/off, brightness, color, scenes Yes (setPilot) Yes
State queries Yes (getPilot) Yes
Discovery (broadcast) Yes (registration) No
Read user config (fade, default dim) Yes (getUserConfig) Yes
Write user config (fade, default dim) No (returns error -32600) Yes (setUserConfig via MQTT)
Fade speed / transitions No Yes
SpaceSense (Wi-Fi motion detection) No Yes (app → cloud → MQTT → bulb)
PIR motion sensor config No Yes
PIR motion events (passive listen) Yes (syncPilot with src: "pir") Yes
Schedules Partial Yes
Firmware updates No Yes (signed, via firmware.wiz.world)
UDP Methods (port 38899)

All methods use JSON. Some are read-only or disabled on UDP at the firmware level (a per-method flag byte controls UDP availability).

Method UDP Description
registration Yes Discovery / heartbeat registration
getPilot Yes Query current light state
setPilot Yes Control light (on/off, color, brightness, scene)
syncPilot Push Heartbeat/state push from bulb (port 38900)
getUserConfig Read-only Returns fadeIn, fadeOut, dftDim, opMode, po, minDimming, tapSensor
setUserConfig Blocked Returns {"error":{"code":-32600,"message":"Invalid Request"}}
getSystemConfig Yes Internal device config (MAC, module, etc.)
setSystemConfig Yes Write internal config
getModelConfig Yes Device model/capabilities
getDevInfo Yes Device info
setDevInfo Yes Write device info
getWifiConfig Yes WiFi settings
setWifiConfig Yes Write WiFi settings
pulse Yes Pulse brightness
reboot Yes Reboot device
reset Yes Factory reset
setState Yes Set state
setSchd Yes Set schedule
setSchdPset Yes Set schedule preset
setFavs Yes Set favorites
firstBeat Push Initial config announcement from bulb
getUserConfig Response Example
{
  "method": "getUserConfig",
  "env": "pro",
  "result": {
    "fadeIn": 700,
    "fadeOut": 700,
    "dftDim": 100,
    "opMode": 0,
    "po": false,
    "minDimming": 0,
    "tapSensor": 1
  }
}
SpaceSense (Wi-Fi Motion Detection)

SpaceSense uses Wi-Fi CSI (Channel State Information) between 2+ bulbs to detect movement — no PIR hardware needed. Configuration is entirely cloud-side (app → Wiz cloud → MQTT → bulbs). There is no known local API to enable, disable, or configure SpaceSense.

The separate Wiz PIR motion sensor accessory communicates via BLE and triggers syncPilot pushes with "src": "pir" that can be passively observed on the local UDP API.

Network Details
  • Port 38899: Bulb receives commands
  • Port 38900: App/client receives heartbeats and push notifications
  • Port 80: Open on bulb but no endpoints discovered
  • MQTT: mqtt.wiz.world / us.mqtt.wiz.world port 8883 (TLS) — cloud connection with device-specific credentials
  • Protocol: Unencrypted JSON over UDP (local), MQTT over TLS (cloud)
  • Chip: ESP8266 (older) / ESP32 (newer, for BLE + CSI support)
Cloud API (WiZ Pro)

The WiZ Pro API uses GraphQL over HTTPS with OAuth 2.0 + PKCE. It requires a clientId obtained from Wiz (no self-service signup). Available mutations are limited to light state control (changeLightState, changeRoomState, etc.) — no setUserConfig or SpaceSense control is exposed.

References

Keywords