The Zengge / Magic Home wire protocol

Everything here was measured against a live controller and checked by reading the state back. Where a published library and the hardware disagreed, the hardware won and the disagreement is noted.

Reference controller: FVTLED / Zengge RGBW, hardware AK001-ZJ21413, model byte 0x06, firmware v4.

Ports

Port Protocol Purpose
5577 TCP Commands and state. No authentication of any kind.
48899 UDP Discovery, and the controller’s wifi setup.
80 TCP A web configuration page on some firmware. Refused on v4.

Anything that can reach port 5577 can drive the lights. There is no password, no token and no rate limit. Treat these controllers as untrusted devices and put them somewhere you are happy for that to be true.

Framing

A message is a few plain bytes with a one-byte checksum appended, which is the sum of every preceding byte, taken modulo 256.

def frame(payload):
    return bytes(payload) + bytes([sum(payload) & 0xFF])

Legacy model 0x01 is the exception: it uses a different, checksum-free encoding.

Message length is per model, and getting it wrong is silent

There are two colour message shapes:

8-byte:  31 R G B W <mask> 0f <checksum>          RGB and RGBW controllers
9-byte:  31 R G B W W2 <mask> 0f <checksum>       RGBWW and RGBCW controllers

A message of the wrong length is not rejected. It is read as a different command. Sending the 9-byte form to an 8-byte controller switched a live unit off — no error, no complaint, just a different outcome from the one asked for. Keep a table of which model takes which, and never guess.

Commands

What Bytes (before the checksum)
On 71 23 0f
Off 71 24 0f
Colour 31 R G B W f0 0f
White 31 00 00 00 W 0f 0f
Both channels 31 R G B W 00 0f — see below
Built-in pattern 61 <pattern> <delay> 0f
Custom pattern 51 … — see below
Read state 81 8a 8b 96 (already checksummed)

The write mask

Byte 6 of an 8-byte colour message decides which half of the fixture it applies to: f0 colour only, 0f white only, 00 both.

00 is not honoured by every model. On a model 0x06 the message is accepted, the controller reports success, and the white channel does not move. That is the worst available failure — a command that looks like it worked. Record per model whether both channels can be driven at once, and where they cannot, apply the one that was actually wanted rather than half-doing the job quietly.

Cool white is not a white channel

An RGBW controller has one white channel, and it is the warm one. What the Magic Home app presents as “cool white” is red, green and blue driven together at equal level:

cool white  =  31 ff ff ff 00 f0 0f
warm white  =  31 00 00 00 ff 0f 0f

Confirmed by setting cool white in the app and reading the frame straight back: R=255 G=255 B=255, white channel 0. There is no second white channel to address on this model. An RGBWW model has one, and it belongs in the W2 byte of the 9-byte form.

The two are mutually exclusive: the fixture shows its colour channels or its white one.

Sending a colour turns the controller on

Measured: with the controller off, a colour message brings it on by itself. An explicit on beforehand is harmless and worth keeping for models that behave otherwise.

Speed

Built-in patterns take a delay byte from 1 (fastest) to 31 (slowest), converted from a 0-100 speed:

delay = int(((100 - speed) * 30) / 100) + 1

Custom patterns

Up to 16 colours, stored in the controller and cycled by it with no further traffic.

51 R G B   00 R G B   00 R G B   …   00 <delay> <transition> ff 0f <checksum>

The first colour carries the 0x51 lead byte and the rest carry 0x00. Unused slots are padded with the (1, 2, 3) sentinel the firmware expects — padding with black shows as a blackout in the sequence. Transition is 3a gradual, 3b jump, 3c strobe.

Reading state

81 8a 8b 96 returns fourteen bytes.

Byte Meaning
0 Always 0x81
1 Model number
2 Power — 0x23 on, 0x24 off
3 Mode — 0x61 static colour, 0x60 custom pattern, 0x25–0x38 a built-in pattern
5 Pattern speed — see below
6, 7, 8 Red, green, blue
9 White
10 Firmware version
11 Second white
12 Write mode — f0 colour, 0f white
13 Checksum of bytes 0-12

Byte 5 is not the raw delay

Widely documented as the delay byte. On this firmware it is not — it reports 100 - 3 × delay. Asking for speeds of 90, 50 and 10 read back as 88, 52 and 16, which invert to exactly 90, 50 and 10. Three times out of three.

The two encodings overlap between 7 and 31, so a byte in that range is genuinely ambiguous and no amount of care resolves it from the byte alone. Only one firmware has been measured, so decode with the measured form and fall back to a raw delay only when the result could not be a valid delay.

Speed is meaningless outside pattern mode. Report nothing there rather than a number — a number that means nothing reads as a measurement.

The controller talks when it was not asked

These controllers push their own state unprompted, wrapped in a header beginning b0 b1 b2 b3 and carrying a sequence number. One reply per request is not a safe assumption.

This matters more than it sounds. A misaligned read produces fourteen bytes that parse perfectly and mean nothing, so the failure is not an error — it is a plausible wrong answer. During development it produced a full page of readings, every one of them noise, with nothing to suggest anything was wrong.

Scan the buffer for a frame, verify its checksum, prefer the newest match, and once the model byte is known, ignore frames that disagree with it.

Only one conversation at a time

The controller accepts a single TCP connection. A second one opened while the first is live does not work, and it fails by refusing commands rather than by refusing to connect.

Discovery

Broadcast the ASCII string HF-A11ASSISTHREAD to UDP 48899. Each controller replies with:

<ip>,<mac>,<hardware id>,<uuid>

for example 192.168.1.50,806A34112233,AK001-ZJ21413,…. The name the Magic Home app shows is the last six characters of the MAC — not a model number, however much it looks like one.

One probe is not enough

UDP broadcast on wifi is sent at the lowest rate with no acknowledgement and no retry, so losing a frame is ordinary rather than exceptional. Measured on a live network: a single probe found the controller roughly half the time. Four probes spread across the listening window found it six sweeps out of six, and five out of five again later.

Two things follow, and both are easy to get wrong:

  • Keep listening after a timeout. Giving up on the first quiet gap also loses a slow controller.
  • Merge the result into whatever you already knew. Never replace it. An empty sweep means “nothing answered this time”, not “there is nothing there”. Replacing a cache with an empty sweep turns one lost frame into a houseful of devices with no address.

Some firmware replies to the discovery port rather than to the port the probe came from, so bind 48899 where you can and carry on unbound where you cannot.

Model numbers

Byte 1 of the state frame.

Model Description Channels Message
0x01 Legacy controller RGB pre-checksum encoding
0x04 Controller RGBW RGBW 8-byte, honours both channels
0x06 Controller RGBW RGBW 8-byte, colour or white
0x07 Controller RGBCW RGBWW 9-byte
0x21 Dimmable white bulb W 8-byte
0x25 Controller RGBWW RGBWW 9-byte
0x27 Warm white controller W 9-byte
0x33 Controller RGB RGB 8-byte
0x35 Bulb RGBWW RGBWW 9-byte
0x44 Bulb RGBW RGBW 8-byte
0x81 Controller RGBW RGBW 8-byte, honours both channels

An unrecognised model is driven with the 8-byte shape, which is the commonest and the safer guess — and it is logged as unrecognised rather than quietly treated as understood.

Built-in patterns

Contiguous, 0x25 to 0x38.

Code Pattern Code Pattern
0x25 Seven colour cross fade 0x2F Green/blue cross fade
0x26 Red gradual change 0x30 Seven colour strobe
0x27 Green gradual change 0x31 Red strobe
0x28 Blue gradual change 0x32 Green strobe
0x29 Yellow gradual change 0x33 Blue strobe
0x2A Cyan gradual change 0x34 Yellow strobe
0x2B Purple gradual change 0x35 Cyan strobe
0x2C White gradual change 0x36 Purple strobe
0x2D Red/green cross fade 0x37 White strobe
0x2E Red/blue cross fade 0x38 Seven colour jumping

Throughput

The reference controller accepted about 40 commands a second sustained. Beyond that, commands are dropped rather than queued, which shows as stutter in a fade. Twenty updates a second is smooth and leaves room to spare.

What the app does that the controller does not

Music and camera modes are the phone doing the work. It samples its own microphone or camera, works out a colour, and streams ordinary colour commands. There is no sound or vision anywhere in the protocol, and nothing in the state frame reports either. Anything that can send colours quickly can do the same thing, given a microphone of its own.