Wyze Local API - Encoding Changes

Hello,

For the last couple of months I’ve been using a “local API” discovered for Wyze color bulbs. I was able to make a simple HTTP POST request to http://bulbip:80/device_request with the following JSON

{"request":"set_status","isSendQueue":0,"characteristics":[{"pid":"P1507","pvalue":"0000ff"}]}

With an update in the last 1-2 weeks, the characteristics field for this “local API” appears to now be encoded, and the bulbs no longer respond to local requests with the plain JSON.

Example captured from an iOS device (using Burp):

{"request":"set_status","isSendQueue":0,"characteristics": "dq0PeZ4yUSCh9nDPvLDdEqm8bpDa7AoA7lHaeQqSSqxN2NfQf0b07TM0YRQGz1jPaKez\/NtExWiEyXkv7fsLLDe5rP\/QaVd+HLpKkhdjf8UM4QKcD5Olwmy0NMRqjlHVYKu0pcnwXolPEj29R+Np7ZJe7HjXazkkuXg9uE0HGpg="}

This new update seems to do the following:

  1. Local bulb requests are changed from port 80 to port 88
  2. Local bulb requests now have an encoded characteristics field.

The issue is trying to figure out what sort of encoding is being used to change what used to be a simple pid and pvalue to a new encoded string.

I’ve tried decoding it as base64 among other things, but to no luck. Additionally, it appears to be using a timestamp perhaps as a hash/salt? Even firing off the same requests from the app produces differing local requests every time.

Examples of local API traffic from the app → bulb
Bulb Off #1

{"isSendQueue":"0","request":"set_status","characteristics":"PeqR2WzXYqvl9ncW5WJyhEgTi1XOP9vGsezVU1yK4K9c4tkCPMswNzdCvOSITv1q1b\/Y\/CBzJ3cprbvEPWuS0\/n3VloMfltK7QNQjejT7uA5d\/I3OjNZVjv5qSf2mWp5"}

Bulb On #1

{"isSendQueue":"0","request":"set_status","characteristics":"PeqR2WzXYqvl9ncW5WJyhEpf4D+53awQCS+BHZrsh\/EDQJrD6DfHt6JWzwUrpFwClCdiziwRN\/DXzTWKMgegoxp8TcI\/fZ5\/HVjUnd5ybKvlXUTuifYkLfXBq3vhHgiv"}

Bulb Off #2

{"isSendQueue":"0","request":"set_status","characteristics":"PeqR2WzXYqvl9ncW5WJyhClwJWs0BjD5SmNN\/N3AGvtDv53zT3b9+XXf9IHEovCi3QlAGrraMuLjrtZIWQ\/mclFvfg4v5KimLi7EfH7zBadFufI4dxQK3+8fFFtmwxfG"}

Bulb On #2

{"isSendQueue":"0","request":"set_status","characteristics":"Gwoyljs4txZqQRsjG3650hrapOfggdB9sHvQiaAEYEhY\/2zkPnhW\/JWapxHvYvgvurUrR4dPFDO52urRKjrLEB1NYKdxa1m2flS\/UYOWP3HUp8R2OiogvDGh+tscQu8k"}

Bulb Off #3

{"isSendQueue":"0","request":"set_status","characteristics":"Gwoyljs4txZqQRsjG3650slXPbIEYF07EB4bGt5jpcO4HW5NZ0hvkJsucXztkzFHoWkSo8XqRdDYX1ASdx+pi9yIsXyvGjXrHVyZanD0WHjich7eT6GQldaky1Qdtx+p"}

Bulb On #3

{"isSendQueue":"0","request":"set_status","characteristics":"Gwoyljs4txZqQRsjG3650jlaMhsws\/odczQ9sU5x+rY2AxIXb\/Te\/MHB4Qner7FFrmwzWH9yenNc6GhNcLuY4dG\/nrOUzp9PfEzbwfvsoopPfqd+sr31h1F9YVUMVWkx"}

I know there is no official API, but it would be amazing to still use this “local API” for our own applications. This way we can send requests entirely locally, without any external rate limiting or strain on Wyze. Does anyone have any pointers on trying to figure out what sort of new encoding is being used on this characteristics field?

Sounds like you’re far ahead of a lot of us, and thanks for the info. I didn’t even know any of the devices were accepting local queries like that. Other than a possible friendly “leak” from a Wyze staffer I don’t know if you’ll get any helpful responses. But I hope you do!

For anyone wondering:

I found this article where Josh decompiled Wyze’s Android app for some more information on how Wyze was encrypting their traffic.

I ended up hiring Josh for a full implementation (of the local network API) with Node JS specifically. However, I can share a bit more of the current process details…

Here’s an example of a local POST:
http://bulbip:88/device_request

{
  "request": "set_status",
  "isSendQueue": 0,
  // The entire characteristics property will be encrypted before it is sent
  "characteristics": {
    "mac": "12345ABCD", // Bulb MAC address
    "index": "1",
    "ts": 1234567890, // Timestamp
    "plist": [
        {
          "pid": "P3", // Power State
          "pvalue": "1" // On/Off
        },
        {
          "pid": "P1507", // Color State
          "pvalue": "0000ff" // HEX Color
        },
        {
          "pid": "P1501", // Brightness 1-100
          "pvalue": "100" // Brightness
        },
    ]
  }
}

Every Wyze bulb now has an enr property. This is used as the “key” and “IV” for AES encryption. Existing libraries for both Node JS and Python can already retrieve this property for the bulb(s).

The characteristics field for the local API is then encrypted with AES CBC, with both the key and IV as the bulb’s enr value.

I assume the characteristics field suddenly being encrypted is for the sake of security. Even though you have to still be on the same (ideally secured) network, my guess is Wyze added this for an additional layer?

For my implementation, it’s amazing to have local network control over these bulbs to change color, power, etc. multiple times a second if needed - without any sort of rate-limiting with external servers.

This is fantastic info and thanks again.

Also, you resisted mentioning how useful this would be in achieving continued local control during a sustained AWS or other Internet outage such as everyone just experienced, but I won’t. :wink:

I’m at risk of getting the necromancer badge, but, here’s a quick python web app I whipped up that uses the information here to provide local control of your bulbs: GitHub - dgmltn/wyze-python: A local python gateway to your wyze color bulbs … thanks a lot for your writeup, @brian11

And the pseudo API still works over a year later? Nice! Thanks for sharing.

I didn’t find anything more recent than this discussion so I’m resurecting it…
I see that these projects (wyze-python, wyze-sdk) were still active a few months ago on github so maybe there is still some hope.

I was able to use the SDK to get my list of devices, etc.
I was also able to modify wyze-python (to use the now mandatory API key) and get a page listing all my devices and available options.

The last step however, = invoking the local URL is failing:

requests.exceptions.ConnectionError: HTTPConnectionPool(host='192.168.86.122', port=88): Max retries exceeded with url: /device_request (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f893be3d0>: Failed to establish a new connection: [Errno 111] Connection refused'))

Is anyone still succesfull reaching the devices through their local URL ?

Cameras are not listening on port 88