Considering developing drivers for Wyze devices

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.

Hey air-body. I created a "Release Thread" in the more appropriate forum. Feel free to move discussion over there: [RELEASE] WyzeHub - Wyze Device Integration (no cams yet)

1 Like

I proceed to the new thread after this but just to close the loop here...

The python package, they refer to it as an SDK

I have both V2 and V3. I got the power on/off to work, didn't realize you had hard coded the model in the driver. Using the model number associated with the V2 camera, I can toggle power on both versions as well as see the event recording settings.

I will do that, still going to test the driver a bit before submitting a pull request.

Ah gotcha. I was hopeful there were some Hubitat dev tools I was missing, ha.

There may be a better way. Could maybe store the possible drivers for a device type in an array. And then store the actual device model somewhere in the device data when adding the device.

It seems most of the time Wyze just ignores whatever you send as the model anyway. They require the value but seemingly ignore it. I was sending this_is_required_but_value_does_not_matter as the model for a bit :wink:

Randomly scanned through this thread, nice license at the top of your code :slight_smile: For some reason I can't remember seeing it as an option in GitHub....

Likely because GitHub isn't the authoritative source for licensing :wink:

It's a fun license. I generally like MIT over Apache ... but DBAD is definitely appropriate for these types of scenarios.

https://dbad-license.org/

2 Likes

Problem is, you still have to keep these cameras connected to the cloud or they will not stream locally either. That's a major hell-no from me, dawg.

Initially responded to check the GitHub package but I see that's what you're referring to. My understanding was the package referenced in the earlier post would potentially allow for local-only RTSP streaming?

The RTSP firmware allows for local only streaming, and IFAIK so does the wrapper software...but you still have to give the camera internet access. Blocking it turns these things into paperweights. I just tested this yesterday.

Were you able to tell what it's talking to and what data it needs back? I wonder if it would be as simple as faking DNS locally.

Based on other people's attempts, it needs a proper check in with the cloud. I gave up and I'm returning them.

I don't know about others, but having integration with Wyze hall effect sensors and motion sensors would be nice.