Considering developing drivers for Wyze devices

Hey ya'll. First post and all that.

I've recently picked up a bunch of Wyze Color WiFi bulbs. From what I can tell, there aren't any functioning Hubitat apps/drivers for Wyze devices. Homebridge has some drivers but not for the color bulb. I started extending that package but realized how limited Homekit is. Not to mention slow.

So I figured I'd look in to putting together some drivers for Hubitat directly. Hoping you can clear up some initial questions.

  • For Background: These devices use a REST api hosted by Wyze. The API authenticates with a username/password (using a non-oauth2 flow), then hands out an access token/refresh token pair. The access token is sent in the body on future requests. I have the API functioning in Postman and can control the bulbs.

  • From what I can tell the API is undocumented. However it's easily reverse engineered using this python package: GitHub - shauntarves/wyze-sdk: A modern Python client for controlling Wyze devices..

  • I've gathered I'll need a Groovy "Application" to collect the user's credentials and provide any configuration options. No issues here and I have an initial version coded for this.

  • I've also gathered I'll need a Groovy "Bridge Device". Most of my questions revolve around this. I'm guessing this is where the "API Service" logic would exist to make the API calls for the bulbs/other devices. It's not clear to me how the bridge device gets the credentials from the app, or how the devices interact with the bridge device. I likely just haven't found a great example yet, or haven't dug deep enough. Any guidance here would be appreciated.

  • Lastly I'll need a driver per device. No questions here yet. I'll start with the Wyze color bulb and may adopt others if I get my hands on some additional devices.

4 Likes

For some of the drivers I have done I didn't include an app, though perhaps I should have.

I have a parent device where the credentials are stored and authentication in managed, connecting to the service I am using, Wyze in your case, then storing the token in a state variable. I don't think it would be an issue doing this in the app, but I've never developed an app myself.

I then have child devices linked to the parent device, which would be bulbs in your case. I have typically built in the API calls in these devices, obtaining the token from the parent device. I have read a recommendation recently that it is best to keep the child devices light and let the parent device in this setup handle the API calls.

Not sure how much help this may be.... May at least offer you an alternative setup if you get stuck. I know I've used apps where the authentication is at least captured in the app, so there must be ways others have handled this.

Simon

If you can get the Wyze v3 camera to work in Hubitat. It would be a miracle.
Because rtsp does not work on v3.

1 Like

Thanks, this was helpful. I've changed my approach a bit to just use the application and child devices for now, and not use the "bridge" device until I find I a need for it.

I think having the configuration and application logic all reside in the application should be fine for an initial integration.

Over the weekend I created the App and got it logging to the Wyze API and pulling devices. Now all that's left is all the work.

I did have some issues with structuring the API request. Namely the httpPost method wasn't converting the body to JSON properly when sending in a Map with the 'requestContentType' : "application/json; charset=utf-8", option also set. The API call would fail every time, but if I set body to (new JsonBuilder(body)).toString() instead, it works. I may make another post about this since the behavior seems inconsistent.

1 Like

If I'm successful in getting the lights going and if I get my hands on some hardware I may move on to Cams. I myself don't have any of the Wyze cams and avoid cloud cam storage in general (requires too much trust in someone else's security. plus it's free for a reason).

The Python SDK makes some reference to v3 cams but I haven't looked to see if it actually supports them. If not there has to be some package on GitHub somewhere with a working example. Definitely in the realm of possibility.

1 Like

Quick update ... I have the beginnings of a functioning Wyze Integration. So far:

  • Can retrieve a list of devices from Wyze
  • Can add supported devices (Currently "Plug" and "MeshLight")
  • Plug can be turned on or off (#somuchfeature)
  • MeshLight (Color Bulb) is proving to be fickle as the original Nintendo system, but making progress.
    • Can turn on and off
    • Can adjust color temp and brightness
    • Color adjustment coming once I understand how to switch the color switcher from "HSL" to "RGB" ... the Wyze API takes HEX codes.
  • Tried to order a non-color light but no luck so far.
  • I currently don't own any Wyze cams so no work on those.
  • Starting an Amazon wish list if anyone who wants to donate any equipment ... no guarantees.

Also. The fact that their own API isn't documented is increasingly pissing me off. The excuses I see posted from their own employees are only more infuriating.

I'm planning to start working on an OpenAPI doc from what the community has reverse engineered to post for future use.

Maybe they'll get the hint.

2 Likes

PM me i have 4 or 5 that i took out of commission because of the null integration with any other system and the arrogance of the wyze people.
BTW most of wyze products are just Tuya devices rebranded to wyze. The V1 and V2 cams were selling way long before wyze even existed but they say they are a brand new company with original ideas BS to the max.
I will gladly send you a V2 camera so you can try to get some development done. I would love to be able to use the remaining cameras but not under wyze .

1 Like

They get the hint they just refuse to do anything about it. They want to have full control. From the way they are going about it i see them going straight to a pay service and screwing everyone like it has happened with so many other company's.

2 Likes

Thanks I may reach out soon when I finish these existing devices. Curious what is desired from the cams and I have no idea if it will be simple or difficult.

Getting close to an initial release. I have a question if anyone can answer:

What's the best way to call methods in the App from a "Grandchild" device?

I'm supporting Device Groups by creating a device for the group and then adding all included devices as children to the Group device. So then when I call parent. from the grand child it no longer points to the app object.

My solution for now is to mimic the methods I'm attempting to call in the "Device Group" device and just proxying the calls to the App.

Is there a non hacky way to do this? I didn't see a way to get the app object getApp() or something.

What everybody want it's RSTP or some kind of way to monitor the camera that it's not with the wize app.

I'll look a little bit in to RTSP. From my very brief search it appears Hubitat doesn't support subscribing to RTSP streams and instead uses a JPG URL? Are there any examples of direct RTSP integration in Habitat you know of?

Also, from this tread, dude says he flashed the firmware on his Wyze cams ... maybe an option?

That being said, I have replaced the firmware on my wyze camera and now it has not only a jpeg url, but a web management page and no longer uses the wyze app.

Even better check out wyze-bridge. Will generate an rtsp stream from the cameras without having to flash new firmware. Works well on my V3's but I think there are a few cameras that are not supported.

I have it running in a docker on my server to play with.

1 Like

If you check out CoCoHue it adds the groups and the devices all from the app, but the devices do not appear as children to the group they are all separate devices. Not really ideal if you have a lot of bulb groups I suppose. If it’s a group of bulbs in the same fixture users may only want the group and not the actual individual devices. That app also uses a bridge device, but there is an actual hue bridge that it talks to so I think that is why it is setup as a separate device.

Good stuff! Should be able to get a v3 cam soon and sounds like I can maybe get my hands on a v2. Hopefully I'll be able to play with this in the coming weeks. Looks promising!

Thanks for the reply! Yeah I've actually been using the CoCoHue app as a reference, haha. It's been quite helpful (and also confusing). I noticed the same regarding it's use of the "bridge" device (represents the actual hue bridge) and how it handles devices.

The way I have my app currently set up, it separates devices belonging to "groups" and unassigned devices separately. When dealing with a group, I create a "Group Device" and then create the individual bulbs or plugs or whatever as child devices under that. This allows me to send commands from the group to all devices in the group fairly easily.

I suppose I could do the same with all devices and groups on the same level ... but the UI is much more messy in that situation (I have a group with 17 bulbs).

I'll try to post screen shots tonight.

The issue I'm currently having is when I have a child device belonging to a group call an asynchronous command in the app (to interact with the API), getting the callback back to the device is tough. I've been just sending the deviceNetworkId up to the App. However, when the device is a child of a group, the app can't find the device using getChildDevice() directly.

So I either need to maintain some context of devices and their parents in the app, or I may just try sending the whole device object up to the App so it can be used in the callback.

Here's the current repo for anyone interested. Just support for plugs and color bulbs for now.

Still a little cleanup work to do before v1 release, specifically menu flow and access token management. But the basic functionality for the plugs and bulbs is there.

Also will be adding required manifests for Hubitat package manager.

everything subject to change. no guarantees in life.

Very nice work. I only have cameras and am interested in being able to automate the enabling of motion recording...so I took the plug driver and edited for the camera. From the SDK, I was able to parse through the properties and can now see the status for on/off, online, motion enabled recording, and sound enabled recording. Unfortunately I cannot get the on/off commands to work. I think it is the same action as the plug but I'm certainly not an expert. I know enough to be dangerous. :grin:

I am willing to update code and test if you have any recommendations.

FWIW, I commented out the refreshing scheduling during testing.
I also had to update the app, adding
'Camera': [label: 'Camera', driver: 'WyzeHub Camera']
to the drivermap so I could install.

/*
 * Import URL: 
 *
 * DON'T BE A DICK PUBLIC LICENSE
 *
 * Version 1.1, December 2016
 *
 * Copyright (C) 2021 Jake Lehner
 * 
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document.
 * 
 * DON'T BE A DICK PUBLIC LICENSE
 * TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 *
 * 1. Do whatever you like with the original work, just don't be a dick.
 * 
 *    Being a dick includes - but is not limited to - the following instances:
 *
 *    1a. Outright copyright infringement - Don't just copy this and change the name.
 *    1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick.
 *    1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick.
 *
 * 2. If you become rich through modifications, related works/services, or supporting the original work,
 *    share the love. Only a dick would make loads off this work and not buy the original work's
 *    creator(s) a pint.
 * 
 * 3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes
 *    you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back.
 *
 */

import groovy.transform.Field

public static String version() {  return "v1.0.5"  }

public String deviceModel() { return 'WLPP1CFH' }

@Field static final String wyze_action_power_on = 'power_on'
@Field static final String wyze_action_power_off = 'power_off'

@Field static final String wyze_property_power = 'P3'
@Field static final String wyze_property_device_online = 'P5'
@Field static final String wyze_property_motion_record = 'P1047'
@Field static final String wyze_property_sound_record = 'P1048'

@Field static final String wyze_property_power_value_on = '1'
@Field static final String wyze_property_power_value_off = '0'
@Field static final String wyze_property_device_online_value_true = '1'
@Field static final String wyze_property_device_online_value_false = '0'
@Field static final String wyze_property_device_motion_record_value_true = '1'
@Field static final String wyze_property_device_motion_record_value_false = '0'
@Field static final String wyze_property_device_sound_record_value_true = '1'
@Field static final String wyze_property_device_sound_record_value_false = '0'

metadata {
	definition(
		name: "WyzeHub Camera", 
		namespace: "jakelehner", 
		author: "Jake Lehner", 
		importUrl: ""
	) {
		capability "Outlet"
		capability "Refresh"

		attribute "motionRecord", "bool"
        attribute "soundRecord", "bool"
		attribute "online", "bool"
	}

	preferences 
	{
		
	}
}

void installed() {
    log.debug "installed()"

	// TODO Make Configurable
	unschedule('refresh')
	//schedule('0/10 * * * * ? *', 'refresh')

    refresh()
	initialize()
}

void updated() {
   log.debug "updated()"
   initialize()
}

void initialize() {
   log.debug "initialize()"
}

void parse(String description) {
	log.warn("Running unimplemented parse for: '${description}'")
}

def refresh() {
	app = getApp()
	logInfo("Refresh Device")
	app.apiGetDevicePropertyList(device.deviceNetworkId, deviceModel()) { propertyList ->
		createDeviceEventsFromPropertyList(propertyList)
	}

	// TODO Make Configurable
	keepFresh = true
	keepFreshSeconds = 10
	//runIn(keepFreshSeconds, 'refresh')
}

def on() {
	app = getApp()
	logInfo("'On' Pressed")

	app.apiRunAction(device.deviceNetworkId, deviceModel(), wyze_action_power_on)
}

def off() {
	app = getApp()
	logInfo("'Off' Pressed")

	app.apiRunAction(device.deviceNetworkId, deviceModel(), wyze_action_power_off)
}

void createDeviceEventsFromPropertyList(List propertyList) {
	app = getApp()
logInfo(propertyList)
    String eventName, eventUnit
    def eventValue // could be String or number

    propertyList.each { property ->
	
		propertyValue = property.value ?: property.pvalue ?: null
		switch(property.pid) {
            // Switch State
			case wyze_action_power_on:
			case wyze_action_power_off:
            case wyze_property_power:
				eventName = "switch"
                eventUnit = null

				if(propertyValue == wyze_property_power_value_on) {
					eventValue = "on"
				} else if(propertyValue == wyze_action_power_on) {
					eventValue = "on"
				} else {
					eventValue = "off"
				}

				if (device.currentValue(eventName) != eventValue) {
					logDebug("Updating Property 'switch' to ${eventValue}")
					app.doSendDeviceEvent(device, eventName, eventValue, eventUnit)
				}
            break
        
            // Device Online
            case wyze_property_device_online:
                eventName = "online"
                eventUnit = null
                eventValue = propertyValue == wyze_property_device_online_value_true ? "true" : "false"
                
				if (device.currentValue(eventName) != eventValue) {
					logDebug("Updating Property 'online' to ${eventValue}")
					app.doSendDeviceEvent(device, eventName, eventValue, eventUnit)
				}
            break

            // Event Recording based on motion
            case wyze_property_motion_record:
                eventName = "motion_enabled"
                eventUnit = null
                eventValue = propertyValue == wyze_property_device_motion_record_value_true ? "true" : "false"

				if (device.currentValue(eventName) != eventValue) {
					logDebug("Updating Property 'motionRecord' to ${eventValue}")
					app.doSendDeviceEvent(device, eventName, eventValue, eventUnit)
				}
            break

            // Event Recording based on sound
            case wyze_property_sound_record:
                eventName = "sound_enabled"
                eventUnit = null
                eventValue = propertyValue == wyze_property_device_sound_record_value_true ? "true" : "false"

				if (device.currentValue(eventName) != eventValue) {
					logDebug("Updating Property 'soundRecord' to ${eventValue}")
					app.doSendDeviceEvent(device, eventName, eventValue, eventUnit)
				}
            break

        }
    }
}

private getApp() {
	app = getParent()
	while(app && app.name != "WyzeHub") {
		app = app.getParent()
	}
	return app
}

private void logDebug(message) {
	app = getApp()
	app.logDebug("[${device.label}] " + message)
}

private void logInfo(message) {
	app = getApp()
	app.logInfo("[${device.label}] " + message)
}

private void logWarn(message) {
	app = getApp()
	app.logWarn("[${device.label}] " + message)
}

private void logError(message) {
	app = getApp()
	app.logError("[${device.label}] " + message)
}

SDK? There's an SDK? :face_with_monocle:

But thanks for the effort! Is this a v2 cam or v3? Or some other model?

And actually, this would be a good opportunity to test how the contribution flow would work. I've added some instructions to the README in GitHub on how to contribute (basically fork, update, pull request). If you wouldn't mind doing that it would help the contribution be a little more "official", allow other contributions, and help me see if I need to have PRs come in to a different branch, etc.