Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

WizScreenSyncController

Control any WiZ bulb or lightstrip, including every individual light zone on a strip, and sync them to your screen colors in real time.

Features:

  • Real-time screen capture light sync
  • Full 12-zone RGBIC support per lightstrip
  • Independent zone width control for each strip segment
  • Supports WiZ lightstrips (12 zones) and WiZ bulbs (single zone)
  • Automatic network discovery of WiZ devices
  • Live output preview bar showing the current strip colors
  • Manual color control mode with color wheel, RGB sliders and HEX input
  • Multi-zone selection
  • Adjustable brightness, FPS and color smoothing
  • Full RGB color calibration
  • System tray support

Download the exe from releases, or grab the source code.

Run this to install the prerequisites before running the .py file:

python -m pip install -r requirements.txt
image

Protocol documentation

WiZ RGBIC strips have per-segment color control over the local network, but it isn't documented anywhere. I reverse engineered it and this is everything I've worked out, all of it tested against real hardware. Anything I haven't actually confirmed is marked as untested instead of guessed at.

Test device

moduleName ESP25_MHORGB_01
fwVersion 1.38.0
Product WiZ RGBIC LEDStrip, 5m section cut down to about 4m
Physical segments 17 (on this specific install, see Segment count)
Transport UDP, port 38899, fire and forget

The short version

sceneId values from 256 up are custom mode slots. They're writable, and when you write one you can attach an elm object carrying per-segment color data. That's how you get manual RGBIC control.

Three things will catch you out, and none of them are obvious:

  1. A slot that already holds a saved custom mode ignores your elm payload and plays the stored mode instead. You need an empty slot.
  2. The device replies {"success": true} either way. That only means the packet parsed, not that anything was applied.
  3. 12 steps is a hard firmware cap. 13 or more gets rejected.

Payload

{
  "method": "setPilot",
  "params": {
    "mac": "<device mac>",
    "state": true,
    "sceneId": 258,
    "elm": {
      "modifier": 100,
      "support": 17,
      "steps": [
        [0, 255, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 1],
        [0, 0, 0, 255, 0, 0, 0, 100, 0, 0, 0, 0, 1]
      ]
    }
  }
}

Sent as compact JSON to <device ip>:38899. No handshake, no registration, no init packet. This works as the very first thing you ever send to the device.

elm fields

Field Value Notes
modifier 100 Static mode, literal per-segment colors, no animation. 110 seems to select gradients and saved effects, where the firmware renders across the whole strip itself. Values other than 100 aren't fully mapped.
support 17 Found by trial and error. Works every time. 0b10001 makes me suspect a capability bitmask but that's unconfirmed.
steps array 1 to 12 step vectors, see below.

Step vector, 13 elements each:

Index Meaning
0 Unknown, held at 0
1 Red, 0-255
2 Green, 0-255
3 Blue, 0-255
4 Unknown, held at 0. Possibly cool white, since WiZ orders channels r,g,b,c,w everywhere else
5 Unknown, held at 0. Possibly warm white
6 Unknown, held at 0
7 Dimming for this step, 0-100
8-11 Unknown, held at 0. Could be timing or fade, since elm also drives animated modes
12 Width, how many physical segments this step covers

Everything marked unknown sits at 0 and the strip works fine, so none of them are needed for static color.

Scene slots

sceneId 256 and up act as custom mode slots. The WiZ app allows up to 10 user modes, so the range is probably 256-265, but only the values below were actually tested.

sceneId Slot state Response What the strip did
257 Held a mode I'd saved in the WiZ app {"success": true} Played the saved mode. elm ignored completely.
258 Empty {"success": true} Rendered my colors correctly.
256 {"success": true} Untested visually, don't rely on this row.

A saved mode beats a written one. The payload gets accepted, acknowledged as successful, and then silently dropped.

Worth spelling out because it looks exactly like the feature having been patched out. It hasn't. Point the same payload at an empty slot and it works.

So don't hardcode a scene ID. Probe 256-265 at startup and use whichever slot the user hasn't filled. Anyone who makes custom modes in the WiZ app will have occupied slots, and they're exactly the people most likely to want RGBIC control.

Step count limit

Tested against an empty slot (258):

Steps Response
11 {"success": true}, renders
12 {"success": true}, renders
13 {"error": {"code": -32602, "message": "Invalid params"}}
14 Same error

12 is a hard firmware cap, not something inherited from the app UI. Fewer than 12 is fine.

Which also means 12 is the maximum number of independently colored regions you can ever get, no matter how long the strip is. Longer strips don't get more colors, they get wider ones.

Note the firmware is willing to return errors. That's what makes the silent drop on an occupied slot so misleading, you get a success response for a payload that did nothing at all.

Width and coverage

Width (index 12) is an absolute count of physical segments, not a relative weight. Steps get laid down in order from the start of the strip, each one consuming its width. Anything past sum(widths) stays dark.

Measured on the 17 segment test strip:

Widths Result
12 steps, all width 1 12 segments lit, 5 dark
[1,1,2,1,2,1,1,2,1,2,1,2], sums to 17 Whole strip lit

So the rule is just sum(widths) == segment count for full coverage.

To fill a strip of any length, spread 12 steps across N segments as evenly as the division allows:

def fill_widths(n_segments, n_steps=12):
    """Widths that sum to exactly n_segments, spread evenly."""
    return [(i + 1) * n_segments // n_steps - i * n_segments // n_steps
            for i in range(n_steps)]

fill_widths(17) gives [1, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2], summing to 17. Always sums to exactly N, never leaves a dark tail, keeps the widths as even as possible.

Segment count

There's no way to ask the device how many segments it has.

The full getSystemConfig response contains mac, homeId, roomId, rgn, moduleName, fwVersion, groupId, ping, accUdpPropRate and rdIdUidHash. Nothing about length, segments or LED count.

You can't infer it from the model either, because these strips are cuttable. The test strip is a 5m unit cut down to roughly 4m and the firmware has no idea. A model to segment count lookup table will be wrong for everyone who cut theirs.

What's probably consistent per product line is density, around 5-6 LEDs per segment on this one. Total segments then just follows from installed length.

Best approach is to calibrate. Light segments one at a time and have the user say where the strip ends, or let them set the widths by hand. This app uses manual sliders, which sidesteps needing the count at all.

Readback

getPilot never echoes the elm block back:

{"result": {"state": true, "sceneId": 257, "speed": 100, "dimming": 100}}

You get the current sceneId but no segment data. So there's no way to inspect what the WiZ app is sending by polling the device, you'd have to capture the UDP traffic.

Other things worth knowing

  • Repeated writes to the same slot apply fine. Streaming at 30+ fps works with no rate limiting and no dropped frames, which is what makes screen sync possible at all.
  • Nothing waits for an ACK. The response is useful for catching malformed payloads but useless for confirming what's actually on the strip.
  • Non-RGBIC devices seem to no-op on an elm payload rather than break. The light goes out but the device stays reachable. Still worth gating on capability, since a dark lamp is a bug from the user's point of view.

Still unknown

  • Step indices 0, 4, 5, 6 and 8-11
  • The full modifier range past 100 (static) and 110 (gradients and effects)
  • What support actually encodes, and whether it has to change alongside modifier
  • Whether sceneId 256 and 259-265 behave the same as 258
  • Whether the 5-6 LEDs per segment density holds on other WiZ RGBIC products, including the TV sync box

Related

  • home-assistant/feature-requests discussion #1313, a request for RGBIC gradient and custom dynamic mode support. Someone there reports the same ESP25_MHORGB_01 module, so this should transfer straight to their hardware.
  • sbidy/pywizlight, which as of 0.6.3 has no elm support and a scene table that stops well below 256, so custom modes can't be expressed through it yet.

Findings are from one device. Corrections and results from other WiZ RGBIC hardware are welcome, especially on the unknown step indices since that's the main gap left.

About

A lightweight Python app that controls wiz RGBIC lightstrips per individual zones and syncs them to your screen, also includes bulb control.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages