Some small edits to the Child app to prevent it throwing an error in the logs if it tries to send a command to a virtual motion device that is not set up (for example, if the camera senses a Pet or Vehicle, but the user did not yet set up those virtual motion sensors in the app for that category of AI object detection). The error in the logs doesn't affect operation of the code, but it's tidier to clean it up
Enjoy!
Updated Child App:
/**
Hubitat App to allow ip/web cameras in tinyCam to drive Hubitat Virtual Motion Sensors
tinyCam is an Android app that can discriminate between different types of motion:
- motion
- person
- pet
- vehicle
- face (non specific/personal)
Set-Up:
In tinyCam:
- install the tinyCam Monitor Pro app (there is a free add-supported version)
- add your cameras
- in camera settings, motion detection, enable all the motion types
- add a webhook http://[your_hub_ip/apps/api/XXXX/?access_token=YYYY&motion=%MOTION_TYPE%
(where XXXX is the ID of this app once installed and YYYY is the access code reported in this app (remember to activate oath when installing)
In Hubitat:
- simply set-up virtual motion sensor devices (1 for each motion type)
- then select these when you run the app
- you can also use an html tile in a dashboard to view the live webcam (if you activate the webserver function in tinyCam)
-----------------------------------------------------------------------------------------------------------------------------
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
for the specific language governing permissions and limitations under the License.
------------------------------------------------------------------------------------------------------------------------------
If modifying this project, please keep the above header intact and add your comments/credits below - Thank you!
------------------------------------------------------------------------------------------------------------------------------
Changes:
0.0.4 - 8th July 2020 - Added conditions on the switches to prevent log errors when trying to address virtual switches that don't exist [Angus Marshall]
0.0.3 - 27th June 2020 - Set up parent/child structure (heavily leveraging @bptworld's great work - thank you!) [Angus Marshall]
0.0.2 - 20th June 2020 - Added selection of virtual motion sensors, simplified url/params, code to activate motion sensors,
code to rename the app [Angus Marshall]
0.0.1 - 18th June 2020 - Initial code [Markus Liljergren]
**/
def setVersion(){
state.name = "tinyCam Connector"
state.version = "0.0.4"
}
definition(
name: "tinyCam Connector Child",
namespace: "Angus_M",
author: "Angus Marshall (Angus_M)",
description: "set virtual motion sensors to active when http received from tinyCam (or other similar webcam app)",
category: "Convenience",
parent: "Angus_M:tinyCam Connector",
iconUrl: "",
iconX2Url: "",
iconX3Url: "",
documentationLink: ""
) {
}
preferences {
page(name: "pageMain", title: "Main Page", install: true, uninstall: true)
page(name: "pageEnableAPI")
page(name: "pageDisableAPI")
mappings {
path("/") {
action: [
GET: "getDevice"
]
}
}
}
void installed() {
display()
log.debug "Installed with settings: ${settings}"
initializeAPIEndpoint()
}
def updated() {
display()
log.debug "Updated with settings: ${settings}"
unsubscribe()
initializeAPIEndpoint()
}
String initializeAPIEndpoint() {
if(!state.accessToken) {
if(createAccessToken() != null) {
state.endpoint = getApiServerUrl()
state.localAPIEndpoint = getFullLocalApiServerUrl()
state.remoteAPIEndpoint = getFullApiServerUrl()
}
}
return state.accessToken
}
/* Pages */
Map pageDisableAPI() {
dynamicPage(name: "pageDisableAPI") {
display()
section() {
if (state.accessToken != null) {
state.accessToken = null
state.endpoint = null
paragraph("SUCCESS: API Access Token REVOKED! Tap Done to continue")
}
}
}
}
Map pageEnableAPI() {
dynamicPage(name: "pageEnableAPI", title: "") {
display()
section() {
if(state.accessToken == null) {
initializeAPIEndpoint()
}
if (state.accessToken == null){
paragraph("FAILURE: API NOT Initialized!")
} else {
paragraph("SUCCESS: API Initialized! Tap Done to continue")
}
}
}
}
Map pageMain() {
dynamicPage(name: "pageMain") {
display()
section() {
if(state.accessToken == null) {
paragraph("API is not yet Initialized!")
href(name: "hrefPageEnableAPI", title: "Enable API", description: "", page: "pageEnableAPI")
} else {
section("Instructions:", hideable: true, hidden: true) {
paragraph("Put the following URL into the tinyCam webhook for motion on the selected camera. This app will read the webhook when transmitted by tinyCam and activate the virtual motion sensor selected below depending on the motion type (eg. simple motion, or AI-identified motion from Person, Vehicle, Pet, or (non-personal) face).")
}
section(getFormat("header-blue", "URLs")) {
String localURL = "${state.localAPIEndpoint}/?access_token=${state.accessToken}&motion=%MOTION_TYPE%"
String remoteURL = "${state.remoteAPIEndpoint}/?access_token=${state.accessToken}&motion=%MOTION_TYPE%"
paragraph("LOCAL API (devices): <a href=\"$localURL\" target=\"_blank\">$localURL</a>")
paragraph("REMOTE API: <a href=\"$remoteURL\" target=\"_blank\">$remoteURL</a>")
}
section(getFormat("header-blue", "Virtual Motion Devices")) {
input "motionDevice", "capability.motionSensor", title: "Motion: Which motion sensor to control?", multiple: false, required: true
input "personDevice", "capability.motionSensor", title: "Person: Which motion sensor to control?", multiple: false
input "petDevice", "capability.motionSensor", title: "Pet: Which motion sensor to control?", multiple: false
input "vehicleDevice", "capability.motionSensor", title: "Vehicle: Which motion sensor to control?", multiple: false
input "faceDevice", "capability.motionSensor", title: "Face: Which motion sensor to control?", multiple: false
}
section(getFormat("header-blue", "Child App name (optional to change)")) {
label title: "Enter a name for this app", required: false
}
}
}
display2()
}
}
Map getDevice() {
log.debug("Running getDevice() $params")
if ((params.motion == "Motion") && (motionDevice)) {
motionDevice.active()
} else if ((params.motion == "Person") && (personDevice)) {
personDevice.active()
} else if ((params.motion == "Pet") && (petDevice)) {
petDevice.active()
} else if ((params.motion == "Vehicle") && (vehicleDevice)) {
vehicleDevice.active()
} else if ((params.motion == "Face") && (faceDevice)) {
faceDevice.active()
}
}
def getFormat(type, myText="") { // Modified from @Stephack Code
if(type == "header-blue") return "<div style='color:#ffffff;font-weight: bold;background-color:#5da2c2;border: 1px solid;box-shadow: 2px 3px 5px 5px #5da2c2'>${myText}</div>"
if(type == "line") return "<hr style='background-color:#5da2c2; height: 1px; border: 0;'>"
if(type == "title") return "<h2 style='color:#5da2c2;font-weight: bold'>${myText}</h2>"
}
def display() {
setVersion()
getHeaderAndFooter()
theName = app.label
if(theName == null || theName == "") theName = "New Child App"
section (getFormat("title", "${state.name} (${theName})")) {
paragraph "${state.headerMessage}"
paragraph getFormat("line")
}
}
def display2() {
section() {
paragraph getFormat("line")
paragraph "<div style='color:#5da2c2;text-align:center;font-size:20px;font-weight:bold'>${state.name} - ${state.version}</div>"
paragraph "${state.footerMessage}"
}
}
def getHeaderAndFooter() {
if(logEnable) log.debug "In getHeaderAndFooter (${state.version})"
def params = [
timeout: 30
]
try {
def result = null
httpGet(params) { resp ->
state.headerMessage = resp.data.headerMessage
state.footerMessage = resp.data.footerMessage
}
if(logEnable) log.debug "In getHeaderAndFooter - headerMessage: ${state.headerMessage}"
if(logEnable) log.debug "In getHeaderAndFooter - footerMessage: ${state.footerMessage}"
}
catch (e) {
state.headerMessage = "<div style='color:#5da2c2'>@Angus_M Apps and Drivers</div>"
state.footerMessage = "<div style='color:#5da2c2;text-align:center'>Angus Marshall<br><a href='https://paypal.me/angusmarshall' target='_blank'><img src='https://www.paypalobjects.com/webstatic/mktg/logo/pp_cc_mark_37x23.jpg' border='0' alt='PayPal Logo'><br>Paypal contributions welcome!</a></div>"
}
}