I’ll start off by making is clear that I’m not advocating for others to adopt this approach. It’s overly reliant on far too many moving parts to be of general practical use and it depends on a number of work arounds. However, some parts of this may be useful to others so I’ll post it here.
Problem Statement
I have a couple of Wyze cameras. I’d like to be able to see stills every ten seconds or so on my MainUI page so I can quickly check to see if the door is open and verify it’s safe to open/close the door.
However, Wyze doesn’t supply an API (which is a shame because I bet there are thousands on this very forum who would buy their devices if there were a binding). They do supply an alternative firmware that enables local access to an RTSP stream but that firmware is not kept up to date and it can cause havoc on your local network. All of the reverse engineered and cracked firmwares have been abandoned. And if you use this firmware, you have to give up on access to any new features or services Wyze provides.
You may have better luck with the alternative firmware. But I’m stubborn and wanted to have access to the streams with the stock firmware.
Unfortunately all the big players in CCTV software such as ZoneMinder, Shinobi, or BlueIris all require one to replace the firmware to enable the RTSP stream. But there is one CCTV program that can access the Wyze camera stream through Wyze’s cloud service, TinyCam Pro.
The problem with TinyCam Pro is it’s an Android only app.
Approach
- Install Android into a VM with Tasker and droid-VNC-NG for some convenience
- Install TinyCam Pro onto that VM
- Configure TinyCam Pro to access the Wyze Cameras (note if you are still on a Nest account it can also connect to Nest Hello and Nest cameras)
- Configure TinyCam Pro’s recording and notification capabilities
- Configure openHAB’s IP Camera binding
- Future work/possibilities
1. Android in a VM
I already have an ESXi server so it made the most sense for me to install Android in a VM using https://www.android-x86.org/. This is based on LineageOS 14.1 which is Android version 7. While that’s not great it’s adequate for our purposes. However, alternative approaches include:
Approach | Advantages | Disadvantages |
---|---|---|
Use an old Android phone or tablet | Less work up front | Awkward form factor, uncomfortable running a server on completely unsupported software |
Use Android on an RPi | Requires a screen, keyboard and mouse or touch screen to set up and configure, no headless config | Requires an RPi 3 or RPi 4 |
Put it on your “production” phone | Less work up front | Disables the camera feed when not on the LAN or end up using lots of mobile data constantly streaming video |
Virtual Machine on a server | Always running and on local RAM, more power than a typical phone | Probably not the best choice unless already running VMs as it’s a lot of work to get set up initially. |
As is clear, I chose the VM option.
I’m running ESXi 6.7 so I followed the instructions at How to Install Android on VMware: A Step-by-Step Guide. Some places where I deviated from those instructions:
- I downloaded and used the 14.1-r5 64-bit ISO file.
- The OS type was "Other 4.x or later Linux (64-bit).
- I gave the VM more local storage than 8 GB, this is after all going to become a CCTV server.
- I also gave the virtual video card a bit more memory, though I don’t know if that does anything worth while.
- When you get to the GRUB menu what you need to do differs slightly from that tutorial. You do need to edit the first entry as described but after making the changes (i.e. replacing “quiet” with “nomodeset xforcevesa”) you need to hit ENTER. Then hit “b” to boot to the newly edited boot config. This will not be saved.
- Pressing CTRL-F1 to open a terminal didn’t work for me, probably because I’m using the ESXi console and the local OS captures the keypresses. So to edit the grub config as described I just rebooted the VM without making changes to the GRUB entry. That prevents the GUI from loaded and gives you a command prompt. There is a Terminal Emulator app which I discovered later that might have also worked without the reboot.
Once you are up and running one of the first things you’ll want to do is prevent the VM from going to sleep. There is no need and it’s a pain to swipe up with the mouse to wake it up. To do this you need to enable the Developer options menu. Open Settings → About Tablet and click on the build number entry seven times. Then you can back up and go into the new Developer options menu and turn off Sleep by toggling on “Stay awake”.
Go through the menu and enable, disable any other settings desired.
Now go through the apps and remove/disable any that you don’t need like the phone app.
If you use static IPs, now is the time to configure the DHCP server or configure a static IP in the WiFi settings of the VM.
Tasker
You do not need Tasker for this. For an app it’s on the expensive side but if you already have paid for it there is no reason not to use it. In this case one of the problems is that we do not have VM Tools installed and we don’t have a physical power button. So the only way to reboot the VM is to do the equivalent of yanking the power. So I create a task in Tasker to reboot the phone and added a shortcut to that on the desktop. Add one for shutdown too if desired.
droid-VNC-NG
This is an app that installs a VNC server on Android and it doesn’t even require root which is nice. This will make it easier to remotely access the VM/computer. It can be found in the Play store or any of the alternative app stores you may prefer.
However note the limitations, it will only work with the soft keyboard in Android and not work with keyboard events from a VNC client. So make sure to keep the keyboard app enabled.
I’ve actually not played with this much as I’m OK accessing it through the ESXi UI.
TinyCam Pro
Install TinyCam Pro. It has been so long I don’t remember the difference between the free and paid app. I use the paid app.
I’m not going to go through everything. There are docs for that. I’m just going to touch on the outlines of the configuration.
- Go to “Manage cameras”
- Click the + icon to create a camera and select “Add IP camera, NVR/DVR”. Note that TinyCam can be used to turn an android phone into a WiFi camera and there is some network discovery built in.
- Give the camera a name and select Wyze Labs as the Camera brand. Select the model of your camera. Keep the protocol as Cloud (if you have RTSP you can skip all this and use the IP Camera Addon in OH for all of this). Enter your Wyze account credentials and under Advanced settings you’ll need to select the camera feed. In my experience you’ll need to figure out which camera goes with which number feed via trial and error.
- Set all your preferred recording settings. I use 8 GB for the recording quota and I’ve a NextCloud service configured for cloud recording. I will probably adjust this as time goes on to keep just a couple days worth of recordings. You can also record to an FTP server on your LAN which might be easier to set up. You will want to get the recordings off the “tablet” for ease of access one way or the other.
- Review and set any Motion Detection and Audio settings desired. Note we will be coming back to configure the Webhook on motion later.
Repeat for each camera you want to use with TinyCam Pro.
Additional settings to consider:
- Under “UI & Behavior” turn on “Auto start sequence mode on live view” to automatically rotate between the cameras, turn on “Auto start live view on boot”, turn on “Auto start background mode on boot” and turn on “Auto start web server on boot”.
- If you are on physical hardware, you might get some improvement by using Hardware for the H.264/H.265 decoder under “Video settings”.
- Under “Recording settings” there are options to set the size of each video size and how much time before and after a motion event to record (only available when background mode is on).
- Under “Web server” you will need to configure a password for the admin user and/or the Guest user.
- Make sure that the Web server is turned on. It will tell you it’s running in a notification and in the UI with the URI for the server.
IP Camera Add-on
My needs for these cameras in openHAB are very modest. All I want is to show the view with a refresh rate of around 10 seconds. There is a ton more one can do with the IP Camera add-on.
- Install the binding
- Create an RTSP/HTTP IP Camera Thing
- Enter all the relevant configuration settings.
- IP Address: hostname or IP address of the Android VM
- Port: 8083 unless changed in TinyCam Pro
- Snapshot URL:
http://<ip address>:<port>/axis-cgi/jpg/image.cgi?camera=<TinyCam Pro camera number>
- MJPEG URL:
http://<ip address>:<port>/axis-cgi/mjpg/video.cgi?camera=<TinyCam Pro camera number>
- Server Port: a unique number not already in use on this machine, each camera needs it’s own port number
- Username:
admin
orguest
depending on how you configured the webserver access in TinyCam Pro - Password: password that corresponds with the username
- Poll Time: smaller number means more resources
- Update Image Channel When: Image channel follows pollImage
I am running in Docker so don’t really have access to ffmpeg so didn’t do much there. Here is an example Thing.
UID: ipcamera:generic:garage
label: Garage Camera
thingTypeUID: ipcamera:generic
configuration:
mjpegOptions: -q:v 5 -r 2 -vf scale=640:-2 -update 1
ipAddress: arachne
mjpegUrl: http://arachne:8083/axis-cgi/mjpg/video.cgi?camera=1
updateImageWhen: "1"
gifPreroll: 0
ffmpegLocation: /usr/bin/ffmpeg
serverPort: 9999
ffmpegOutput: /etc/openhab/html/camera1/
ipWhitelist: DISABLE
mp4OutOptions: -c:v copy -c:a copy
pollTime: 10000
password: password
port: 8083
snapshotUrl: http://arachne:8083/axis-cgi/jpg/image.cgi?camera=1
snapshotOptions: -an -vsync vfr -q:v 2 -update 1
username: admin
ffmpegInput: http://arachne:8083/axis-cgi/mjpg/video.cgi?camera=1
hlsOutOptions: -strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy
-hls_flags delete_segments -hls_time 2 -hls_list_size 4
gifOutOptions: -r 2 -filter_complex
scale=-2:360:flags=lanczos,setpts=0.5*PTS,split[o1][o2];[o1]palettegen[p];[o2]fifo[o3];[o3][p]paletteuse
Given my use case I’ve only linked Items to two of the Channels.
Item Type | Channel | Purpose |
---|---|---|
Switch | Poll Image | When ON the binding will poll for the latest frame from the camera based on the Poll Time setting |
Image | Image | When the Poll Image Item is ON, it will be populated with the actual latest image from the camera based on the Poll Time setting |
If all went to plan the Image Item will start to be set to the camera feed periodically when the Switch is turned ON.
Page Widget
To show how I’m using this here is the widget I created to put on my overview page.
This shows the camera in the Garage and lets me trigger the garage door openers.
NOTE: Make sure the date/time get’s stamped on the images in the Wyze app or in TinyCam Pro so you can tell if you are looking at a recent view.
uid: garage_widget
tags:
- card
- garage
props:
parameters: []
parameterGroups: []
timestamp: Feb 12, 2021, 10:02:20 AM
component: f7-card
config:
title: Garage Doors
slots:
default:
- component: f7-row
slots:
default:
- component: oh-image
config:
item: GarageCamera_Image
style:
width: 100%
height: auto
- component: f7-row
config:
class:
- justify-content-left
slots:
default:
- component: f7-col
slots:
default:
- component: oh-label-card
config:
footer: Small Garage Door Opener
action: command
actionItem: Small_Garagedoor_Opener
actionCommand: ON
label: '=(items.Small_Garagedoor_Sensor.state == "OPEN") ? "close" : "open"'
item: Small_Garagedoor_Sensor
icon: '=(items.Small_Garagedoor_Sensor.state == "CLOSED") ? "f7:house" : "f7:house_fill"'
iconColor: '=(items.Small_Garagedoor_Sensor.state == "CLOSED") ? "green" : "orange"'
- component: f7-col
slots:
default:
- component: oh-label-card
config:
footer: Large Garage Door Opener
action: command
actionItem: Large_Garagedoor_Opener
actionCommand: ON
label: '=(items.Large_Garagedoor_Sensor.state == "OPEN") ? "close" : "open"'
icon: '=(items.Large_Garagedoor_Sensor.state == "CLOSED") ? "f7:house" : "f7:house_fill"'
iconColor: '=(items.Large_Garagedoor_Sensor.state == "CLOSED") ? "green" : "orange"'
Webhook Config (may not be working)
TinyCam supports webhooks but it only supports HTTP GET commands. OH doesn’t support updating Items with GET commands. However, one way to work around that is to build an HTML page with a little bit of JavaScript to parse out the arguments passed in the URL and make the correct OH REST API call for us. This is based on the work found at OAuth2 using just OH Rules and myopenhab.org.
Create a String Item for each camera which will receive the motion events from TinyCam Pro. I’ll use GarageCamera_Motion
.
Next navigate to each camera’s settings and open the Motion detection settings. Configure the Webhook on motion setting to
http://<ip of openHAB>:8080/static/tinycam.html?item=<item name>&motion=%MOTION_TYPE%
where <ip of openHAB> is the IP or hostname of the openHAB server and
` is the name of the Item that captures the motion detections from this camera.
Copy the following into $OH_CONF/html/tinycam.html
. This will be a web page that will receive the GET request from TinyCam, parse out the arguments in the address and command the item with the value of motion.
<!DOCTYPE html>
<html>
<head>
<title>OAuth2 Catcher</title>
<script type="text/javascript">
function getAllUrlParams(url) {
// get query string from url (optional) or window
var queryString = url ? url.split('?')[1] : window.location.search.slice(1);
// we'll store the parameters here
var obj = {};
// if query string exists
if (queryString) {
// stuff after # is not part of query string, so get rid of it
queryString = queryString.split('#')[0];
// split our query string into its component parts
var arr = queryString.split('&');
for (var i=0; i<arr.length; i++) {
// separate the keys and the values
var a = arr[i].split('=');
// in case params look like: list[]=thing1&list[]=thing2
var paramNum = undefined;
var paramName = a[0].replace(/\[\d*\]/, function(v) {
paramNum = v.slice(1,-1);
return '';
});
// set parameter value (use 'true' if empty)
var paramValue = typeof(a[1])==='undefined' ? true : a[1];
// if parameter name already exists
if (obj[paramName]) {
// convert value to array (if still string)
if (typeof obj[paramName] === 'string') {
obj[paramName] = [obj[paramName]];
}
// if no array index number specified...
if (typeof paramNum === 'undefined') {
// put the value on the end of the array
obj[paramName].push(paramValue);
}
// if array index number specified...
else {
// put the value at that index number
obj[paramName][paramNum] = paramValue;
}
}
// if param name doesn't exist yet, set it
else {
obj[paramName] = paramValue;
}
}
}
return obj;
}
var code = getAllUrlParams().code;
</script>
</head>
<script>
var params = getAllUrlParams();
var item = params.item
var cmd = params.motion
document.write("Preparing to command " + item + " with " + cmd);
var xhr = new XMLHttpRequest();
xhr.open('POST', "http://argus:8080/rest/items/"+item);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.setRequestHeader("Accept", "application/json");
xhr.onreadystatechange = function() {
if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
document.write("Success! Commanded " + items + " to " + cmd);
}
else if(xhr.status != 200) {
document.write("Error: " + xhr.status + " " + xhr.responseText);
}
}
document.write("Sent " + cmd + " to " + item);
xhr.send(cmd);
</script>
</html>
NOTE: This is really easy to make generic to support converting any HTTP GET request to an Item command or update. I might post a separate tutorial on that.
You will need to modify the URL to be the path to your openHAB instance. Note that this will be executed on the caller’s machine so that needs to be the URL that TinyCam can use to reach openHAB.
Future Work
There may be other cases where the only way a device can be interacted with is through its Android app. This approach could be use to host that app and then use Tasker and various Tasker add-ons like AutoNotification and AutoInput or it’s relatively new ability to read and react to the system log to interact with the device’s Android app and send/receive information from openHAB.
That of course is an exercise for the reader.