MSA-1939: BlueIris is a well known security camera management software.

This App+Handler bundle allows to integrate it with Samsung SmartThings, allowing to to manage Blue Iris signal and profile state via SmartThings app - either manually or automatically, by subscribing to location events (like Home, Away, etc).
The main difference with existing integration (by @pursual) is that this one works locally, thus eliminating need to create a potential security breach by exposing ports to internet (it works without any port forwarding). Of course it means BlueIris server should reside on the same network segment as SmartThings Hub.
This commit is contained in:
Nicolas Neverov
2017-04-30 06:07:00 -07:00
parent 7e8baeeb0b
commit 7b683677d1
2 changed files with 943 additions and 0 deletions

View File

@@ -0,0 +1,384 @@
/**
* Copyright 2015 SmartThings
*
* 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.
*
* BlueIris (LocalConnect)
*
* Author: df
* Date: 2015-12-30
*/
definition(
name: "BlueIris (LocalConnect2)",
namespace: "df",
author: "df",
description: "BlueIris local integration",
category: "Safety & Security",
singleInstance: true,
iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.doors.garage.garage-closed",
iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.doors.garage.garage-closed?displaySize=2x"
)
preferences {
page(name: "setup", title: "Blue Iris Setup", content: "pageSetupCallback")
page(name: "mode", title: "Blue Iris Modes Setup", content: "renderModePage")
page(name: "validate", title: "Blue Iris Setup", content: "pageValidateCallback")
}
def switchHandler(evt)
{
log.debug "setupDevice: switch event: $evt.value"
}
private Map getValidators()
{
return [
hostAddress: { addr ->
return addr ==~ /\d+\.\d+\.\d+\.\d+(:\d+)?/
},
mode: { Map p ->
def rc = []
if (p.aProfileApply) {
if (p.aProfile == null) {
rc.push("Arming profile is required");
} else if (p.aProfile < 1 || p.aProfile > 5) {
rc.push("Arming profile must be within [1-5] range")
}
}
if (p.dProfileApply) {
if (p.dProfile == null) {
rc.push("Disarming profile is required");
} else if (p.dProfile < 1 || p.dProfile > 5) {
rc.push("Disarming profile must be within [1-5] range")
}
}
def armProfile = p.aProfileApply ? p.aProfile : 0;
def disarmProfile = p.dProfileApply ? p.dProfile : 0;
if (p.aSignal == p.dSignal && armProfile == disarmProfile && armProfile != null) {
rc.push("Arming and disarming signal/profile combinations must differ")
}
return rc
}
]
}
def pageSetupCallback()
{
if (canInstallLabs()) {
log.debug("pageSetupCallback: refreshing")
def v = getValidators()
return dynamicPage(name:"setup", title:"Setting up Blue Iris integration", nextPage:"", install: false, uninstall: true) {
section("New BlueIris Server setup") {
input name:"devicename", type:"text", title: "Device name", required:true, defaultValue: "Blue Iris Server"
input name:"hub", type:"hub", title: "Hub gateway", required:true
input name:"ip", type:"text", title: "IP address:port", required:true, submitOnChange:true
if (!v.hostAddress(ip)) {
paragraph(required:true, "Please specify valid IP address")
}
input name:"username", type:"text", title: "Username", required:true, autoCorrect:false
input name:"password", type:"password", title: "Password", required:true, autoCorrect:false
}
if(v.hostAddress(ip)) {
section("") {
href(title:"Next", description:"", page:"mode", required:true)
}
}
}
} else {
return dynamicPage(name:"setup", title:"Upgrade needed", nextPage:"", install:false, uninstall: true) {
section("Upgrade needed") {
paragraph "Hub firmware needs to be upgraded"
}
}
}
}
private makeProfileInput(inputName)
{
input(name:inputName.toString(), type:"number", title:"Select profile [1-5]:", range:"1..5", submitOnChange:true, required:true)
}
def renderModePage() {
def v = getValidators()
return dynamicPage(name:"mode", title:"Setting up Blue Iris modes", nextPage:"", install: false, uninstall: true) {
section(hideable:true, "Arming modes") {
input(name:"armSignal", type:"enum", title:"When Armed, set signal to", options:["Green","N/A"], defaultValue:"Green", submitOnChange:true, required:false)
input(name:"armProfileApply", type:"bool", title:"Also, change profile?", defaultValue:false, submitOnChange:true)
if (armProfileApply) {
makeProfileInput("armProfile")
}
input(name:"disarmSignal", type:"enum", title:"When Disarmed, set signal to", options: ["Red", "N/A"], defaultValue:"Red", submitOnChange:true, required:false)
input(name:"disarmProfileApply", type:"bool", title:"Also, change profile?", defaultValue:false, submitOnChange:true)
if (disarmProfileApply) {
makeProfileInput("disarmProfile")
}
}
section(hideable:true, "Location modes") {
location.modes.each {mode->
input(name:"locationSignal${mode.id}".toString(), type:"enum", title:"When in \"$mode.name\" mode, set signal to", options: ["Green", "Red", "N/A"], defaultValue:"N/A", required:false, submitOnChange:true)
input(name:"locationProfileApply${mode.id}".toString(), type:"bool", title:"Also, change profile?", defaultValue:false, required:false, submitOnChange:true)
if (settings["locationProfileApply${mode.id}".toString()] == true) {
makeProfileInput("locationProfile${mode.id}")
}
}
}
def p = [
aSignal: armSignal,
aProfileApply: armProfileApply,
aProfile: armProfile,
dSignal: disarmSignal,
dProfileApply: disarmProfileApply,
dProfile: disarmProfile
]
def valRc = v.mode(p)
if (valRc) {
section("Please correct errors:") {
valRc.each {err ->
paragraph(required:true, "*** $err")
}
}
} else {
section("") {
href(title:"Next", description:"", page:"validate", required:true)
}
}
}
}
def pageValidateCallback()
{
if(ip ==~ /\d+\.\d+\.\d+\.\d+(:\d+)?/) {
return dynamicPage(name:"validate", title:"Setting up Blue Iris integration", install:true, uninstall:false) {
section() {
paragraph(
image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
title:"Ready to install",
"Press 'Done' to confirm installation"
)
}
}
} else {
return dynamicPage(name:"validate", title:"Setting up Blue Iris", nextPage:"", install: false, uninstall:false) {
section("Error validating setup preferences") {
paragraph(
image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
title:"IP Address",
required:true,
"Should look similar to 111.222.333.555:8001 (port is optional)"
)
}
}
}
}
def installed()
{
log.debug("installed: started") //with $settings
init()
}
def updated()
{
log.debug("updated: started"); //with $settings
uninit();
init()
}
def uninstalled()
{
uninit(false);
}
def init()
{
if(!state.subscribed) {
subscribe(location, "mode", modeChangeHandler)
state.subscribed = true
}
state.config = assembleConfig()
final dni = ipEpToHex(ip)
def d = getChildDevice(dni)
if(d) {
log.debug("init: deleting existing BlueIris Server device, dni:$dni")
deleteChildDevice(dni)
}
if(true) {
log.debug "init: adding new BlueIris Server device, dni:$dni, username:$username, password:*****, gateway hub id:$hub.id"
d = addChildDevice("df", "blueiris2", dni, hub.id,
[name:"blueiris", label: devicename, completedSetup:true,
"preferences":["username":username, "password":password]
])
d.configure()
subscribe(d, "switch", switchHandler)
} else {
log.debug "init: skipping adding BlueIris Server device, dni:$dni - already exists"
}
}
def uninit(boolean f_unsubscribe = true)
{
if(state.subscribed) {
if(f_unsubscribe) {
unsubscribe()
}
getAllChildDevices().each {
}
state.subscribed = false
}
}
def modeChangeHandler(evt)
{
def f_arm = (evt.value == 'Away')
log.debug("modeChangeHandler: detected mode change: $evt.name:$evt.value: ${f_arm ? 'arming' : 'disarming'}")
def mode = null
location.modes.each {m->
if (m.name == evt.value) {
mode = m
}
}
getAllChildDevices().each {
it.location(mode.id)
}
}
def asyncOpCallback()
{
log.debug("asyncOpCallback: timeout:$atomicState.asyncOpTimeout, ${now() - atomicState.asyncOpTs}(msec) elapsed")
if(atomicState.asyncOpTimeout) {
getAllChildDevices().each {
it.timeout()
}
}
}
def onBeginAsyncOp(int timeout_ms)
{
log.debug("onBeginAsyncOp: ${now()}")
atomicState.asyncOpTimeout = true
atomicState.asyncOpTs = now()
runOnce(new Date(now() + timeout_ms), asyncOpCallback, [overwrite: true])
}
def onEndAsyncOp()
{
log.debug("onEndAsyncOp: ${now()}")
atomicState.asyncOpTimeout = false
runOnce(new Date(now() + 1), asyncOpCallback, [overwrite: true])
}
def onNotification(msg)
{
log.debug("sendNotification: sending $msg")
sendNotificationEvent(msg)
}
def onGetConfig()
{
return state.config
}
private assembleConfig()
{
def getElementCfg = {prefix, id, name ->
def signal = settings["${prefix}Signal${id}".toString()]
def profileApply = settings["${prefix}ProfileApply${id}".toString()]
def profile = settings["${prefix}Profile${id}".toString()]
return signal != 'N/A' || profileApply ? [
name: name,
signal: signal == 'N/A' ? null : (signal == "Green"),
profile: profileApply ? profile : null
] : null
}
def rc = [
arming: [
arm: getElementCfg('arm', '', 'Arm'),
disarm: getElementCfg('disarm', '', 'Disarm')
],
location: [:]
]
location.modes.each {mode->
rc.location["$mode.id".toString()] = getElementCfg('location', mode.id, mode.name)
}
log.info("onGetConfig: assembled config: [$rc]")
rc
}
private Boolean canInstallLabs()
{
return hasAllHubsOver("000.011.00603")
}
private Boolean hasAllHubsOver(String desiredFirmware)
{
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
}
private List getRealHubFirmwareVersions()
{
return location.hubs*.firmwareVersionString.findAll { it }
}
private String ipEpToHex(ep) {
final parts = ep.split(':');
final ipHex = parts[0].tokenize('.').collect{ String.format('%02X', it.toInteger() ) }.join()
final portHex = String.format('%04X', (parts[1]?:80).toInteger())
return "$ipHex:$portHex"
}
private String hexToString(String txtInHex)
{
byte [] txtInByte = new byte [txtInHex.length() / 2];
int j = 0;
for (int i = 0; i < txtInHex.length(); i += 2)
{
txtInByte[j++] = Byte.parseByte(txtInHex.substring(i, i + 2), 16);
}
return new String(txtInByte);
}