mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-11 05:11:51 +00:00
Compare commits
17 Commits
MSA-1918-2
...
MSA-1939-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04e098603d | ||
|
|
7b683677d1 | ||
|
|
7e8baeeb0b | ||
|
|
62991f8d23 | ||
|
|
11f2775568 | ||
|
|
83b65c0d87 | ||
|
|
e641759a47 | ||
|
|
7820b39b2b | ||
|
|
6a76a8ee39 | ||
|
|
c864fc521e | ||
|
|
016425b7c8 | ||
|
|
c164b201ca | ||
|
|
030dd47b69 | ||
|
|
cc68534b47 | ||
|
|
5e07494dff | ||
|
|
573630232f | ||
|
|
fc32031555 |
559
devicetypes/df/blueiris2.src/blueiris2.groovy
Normal file
559
devicetypes/df/blueiris2.src/blueiris2.groovy
Normal file
@@ -0,0 +1,559 @@
|
||||
/**
|
||||
* 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 (LocalConnect2)
|
||||
*
|
||||
* Author: Nicolas Neverov
|
||||
* Date: 2017-04-30
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "blueiris2", namespace: "df", author: "df") {
|
||||
capability "Sensor"
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
attribute "state", "enum", ["disarmed", "arming", "armed", "disarming", "unknown"]
|
||||
attribute "status", "string"
|
||||
command "arm"
|
||||
command "disarm"
|
||||
command "location" "STRING"
|
||||
command "retry"
|
||||
command "timeout"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input name:"username", type:"text", title: "Username", description: "BlueIris Username", required: true
|
||||
input name:"password", type:"password", title: "Password", description: "BlueIris Password", required: true
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"bi_detail_tile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.state", key: "PRIMARY_CONTROL") {
|
||||
attributeState "disarmed", label:"Disarmed", action:"arm", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
|
||||
attributeState "arming", label:"Arming...", action:"arm", icon:"st.locks.lock.locked", backgroundColor:"#79b821"
|
||||
attributeState "armed", label:"Armed", action:"disarm", icon:"st.locks.lock.locked", backgroundColor:"#79b821"
|
||||
attributeState "disarming", label:"Disarming...", action:"disarm", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
|
||||
attributeState "unknown", label:"Unknown", action:"retry", icon:"st.locks.lock.unknown", backgroundColor:"#ff0000"
|
||||
attributeState "refreshing", action:"Refresh.refresh", icon:"st.secondary.refresh", backgroundColor:"#ffffff"
|
||||
}
|
||||
tileAttribute("device.status", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}', defaultState:true)
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("bi_refresh_tile", "device.refresh", decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"Refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "bi_detail_tile"
|
||||
details(["bi_detail_tile", "bi_refresh_tile"])
|
||||
}
|
||||
}
|
||||
|
||||
def fsmExecInternal(fsmDefinition, stateId, Map params)
|
||||
{
|
||||
def actionResult = null;
|
||||
|
||||
def fsmState = fsmDefinition[stateId?.toString()]
|
||||
if(fsmState == null || fsmState.isFinal) {
|
||||
return [error:"fsmExecInternal: state [$stateId] ${fsmState == null ? 'does not exist' : 'is final'} and cannot be actioned upon"]
|
||||
}
|
||||
|
||||
while(!fsmState.isFinal && !actionResult?.isAsync) {
|
||||
actionResult = fsmState.action(params)
|
||||
fsmState = fsmDefinition[actionResult.nextStateId?.toString()]
|
||||
if(fsmState == null) {
|
||||
return [error:"fsmExecInternal: state [${actionResult.nextStateId}] does not exist and cannot be actioned upon"]
|
||||
}
|
||||
log.debug("fsmExecInternal: transitioned state [$stateId] to [$actionResult.nextStateId] (isFinal:${fsmState.isFinal?:false}); result isAsync:${actionResult.isAsync?:false}")
|
||||
stateId = actionResult.nextStateId
|
||||
}
|
||||
return [actionResult:actionResult]
|
||||
}
|
||||
|
||||
def fsmGetStateId(Map persistentStg)
|
||||
{
|
||||
persistentStg.fsmState
|
||||
}
|
||||
|
||||
def fsmExec(Map persistentStg, fsmDefinition, Map params = null)
|
||||
{
|
||||
def stateId = fsmGetStateId(persistentStg) ?: params.fsmInitialState
|
||||
if(!stateId) {
|
||||
return [error: "fsmExec: cannot determine initial state, must be specified via params.fsmInitialState"]
|
||||
}
|
||||
|
||||
log.debug("fsmExec: ${persistentStg.fsmState ? 'proceeding' : 'starting'} with fsm state [$stateId]")
|
||||
params = (params != null ? params : [:])
|
||||
params.persistentStg = (params.persistentStg != null ? params.persistentStg : persistentStg)
|
||||
|
||||
def rc = fsmExecInternal(fsmDefinition, stateId, params)
|
||||
if(rc.actionResult) {
|
||||
persistentStg.fsmState = rc.actionResult.nextStateId
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
|
||||
def getBlueIrisHubAction(Map body)
|
||||
{
|
||||
final host = getHostAddress();
|
||||
final path = "/json"
|
||||
|
||||
def hubAction = new physicalgraph.device.HubAction(
|
||||
method: "POST",
|
||||
path: path,
|
||||
headers: [HOST:host],
|
||||
body: body
|
||||
)
|
||||
log.info "getBlueIrisHubAction: prepared hubaction $hubAction, requestId: $hubAction.requestId"
|
||||
|
||||
hubAction
|
||||
}
|
||||
|
||||
def sendError(statusMsg)
|
||||
{
|
||||
sendEvent(name:"status", value: statusMsg)
|
||||
sendEvent(name:"state", value: "unknown")
|
||||
parent.onNotification("$device.displayName: $statusMsg");
|
||||
}
|
||||
|
||||
def sendEventInit(cmd)
|
||||
{
|
||||
def state;
|
||||
switch(cmd.id) {
|
||||
case 'status':
|
||||
state = 'unknown'
|
||||
break
|
||||
case 'set':
|
||||
state = (cmd.workflow == 'arm' ? 'arming' : 'disarming')
|
||||
break
|
||||
default:
|
||||
state = 'unknown' //TODO: error
|
||||
}
|
||||
sendEvent(name:"state", value: state)
|
||||
sendEvent(name:"status", value: "Logging in...")
|
||||
}
|
||||
|
||||
def sendEventLogin(cmd)
|
||||
{
|
||||
sendEvent(name:"status", value: "Opening session...")
|
||||
}
|
||||
|
||||
def sendEventStatus(cmd)
|
||||
{
|
||||
sendEvent(name:"status", value: "Getting status...")
|
||||
}
|
||||
|
||||
def sendEventSet(cmd)
|
||||
{
|
||||
log.debug("sendEventSet: executing ${cmd.workflow} workflow, ${cmd.context ? ('context: ' + cmd.context + ',') : ''} signal:${cmd.signal}, profile:${cmd.profile}")
|
||||
sendEvent(name:"status", value: "Executing '${cmd.workflow.toString() == 'arm' ? 'arm' : 'disarm'}${cmd.context ? ' ' + cmd.context : ''}' command")
|
||||
}
|
||||
|
||||
def sendEventFinalize(cmd, signal, profile, workflow)
|
||||
{
|
||||
def isArmed = (workflow.toString() == 'arm')
|
||||
def statusMsg = null
|
||||
def statusDetails = null
|
||||
if(cmd.id == 'set') {
|
||||
statusMsg = "Successfully ${isArmed ? 'armed' : 'disarmed'}${cmd.context ? ' \'' + cmd.context + '\'' : ''} ..."
|
||||
statusDetails = "Successfully ${isArmed ? 'armed' : 'disarmed'} ${cmd.context ? '\'' + cmd.context + '\'' : ''}"
|
||||
} else if(cmd.id == 'status') {
|
||||
statusMsg = "Status is '${isArmed ? 'Armed' : 'Disarmed'}'"
|
||||
statusDetails = "Current status is '${isArmed ? 'Armed' : 'Disarmed'}' [signal:${signal ? 'green' : 'red'}, profile:${profile}]"
|
||||
}
|
||||
|
||||
sendEvent(name:"state", value: isArmed ? "armed" : "disarmed")
|
||||
sendEvent(name:"status", value: statusMsg)
|
||||
parent.onNotification("${device.displayName}: ${statusDetails}");
|
||||
}
|
||||
|
||||
|
||||
def getBIfFsmDef()
|
||||
{
|
||||
[
|
||||
init: [
|
||||
action: {Map params ->
|
||||
def cmd = params.cmd
|
||||
params.persistentStg.cmd = params.cmd
|
||||
sendEventInit(cmd)
|
||||
parent.onBeginAsyncOp(getOperationTimeoutMs())
|
||||
def haction = getBlueIrisHubAction([cmd: 'login']);
|
||||
cmd.requestId = haction.requestId
|
||||
|
||||
[nextStateId:'login', isAsync:true, hubAction:haction]
|
||||
}
|
||||
],
|
||||
login: [
|
||||
action: {Map params ->
|
||||
def cmd = params.persistentStg.cmd
|
||||
def respData = params.respMsg.data
|
||||
|
||||
if(respData?.result == 'fail' && respData.session) {
|
||||
sendEventLogin(cmd)
|
||||
|
||||
final u = settings.username
|
||||
final p = settings.password
|
||||
final token = "$u:${respData.session}:$p"
|
||||
log.debug("getBIfFsmDef[login]: logging in user \"$u\"")
|
||||
def haction = getBlueIrisHubAction([cmd: 'login', session: "${respData.session}", response: "${token.encodeAsMD5()}"]);
|
||||
cmd.requestId = haction.requestId
|
||||
cmd.session = respData.session
|
||||
[nextStateId: (cmd.id == 'set' ? 'set' : 'status'), isAsync:true, hubAction: haction]
|
||||
} else {
|
||||
log.error("getBIfFsmDef[login]: error: unexpected result from login call: $params.respMsg.data")
|
||||
parent.onEndAsyncOp()
|
||||
sendError("Error logging in: unexpected result $params.respMsg.data?.result")
|
||||
[nextStateId: 'error']
|
||||
}
|
||||
}
|
||||
],
|
||||
status: [
|
||||
action: {Map params ->
|
||||
def cmd = params.persistentStg.cmd
|
||||
def respData = params.respMsg.data
|
||||
if(respData?.result == 'success') {
|
||||
sendEventStatus(cmd)
|
||||
def haction = getBlueIrisHubAction([cmd: 'status', session: "${cmd.session}"]);
|
||||
cmd.requestId = haction.requestId
|
||||
[nextStateId: 'finalize', isAsync:true, hubAction: haction]
|
||||
} else {
|
||||
parent.onEndAsyncOp()
|
||||
log.error("getBIfFsmDef[status]: error creating session: unsuccessful result from session login call: ${respData}");
|
||||
sendError("Error establishing session: ${respData?.data?.reason}")
|
||||
[nextStateId:'error']
|
||||
}
|
||||
}
|
||||
],
|
||||
set: [
|
||||
action: {Map params ->
|
||||
def cmd = params.persistentStg.cmd
|
||||
def respData = params.respMsg.data
|
||||
if(respData?.result == 'success') {
|
||||
sendEventSet(cmd)
|
||||
def hubActionParams = [cmd: 'status', session: "${cmd.session}"]
|
||||
if(cmd.signal != null) {
|
||||
hubActionParams.signal = cmd.signal ? 1 : 0
|
||||
}
|
||||
if(cmd.profile != null) {
|
||||
hubActionParams.profile = cmd.profile
|
||||
}
|
||||
def haction = getBlueIrisHubAction(hubActionParams);
|
||||
cmd.requestId = haction.requestId
|
||||
[nextStateId:'finalize', isAsync:true, hubAction: haction]
|
||||
} else {
|
||||
parent.onEndAsyncOp()
|
||||
log.error("getBIfFsmDef[action]: error creating session: unsuccessful result from session login call: ${respData}");
|
||||
sendError("Error establishing session: ${respData.data?.reason}")
|
||||
[nextStateId:'error']
|
||||
}
|
||||
}
|
||||
],
|
||||
finalize: [
|
||||
action: {Map params ->
|
||||
boolean success = false
|
||||
final cmd = params.persistentStg.cmd
|
||||
final respData = params.respMsg.data
|
||||
|
||||
if(respData?.result == 'success') {
|
||||
|
||||
def respSignal = (respData.data?.signal != null ? respData.data.signal == '1' : null);
|
||||
def respProfile = respData.data?.profile
|
||||
|
||||
log.debug("getBIfFsmDef[finalize]: received 'success' response status, finalizing command ${cmd}, "
|
||||
+ "response: [signal:${respSignal}, profile:${respProfile}]")
|
||||
|
||||
if( cmd.id == 'set'
|
||||
&& (cmd.signal == null || cmd.signal == respSignal)
|
||||
&& (cmd.profile == null || respProfile == cmd.profile.toString())) {
|
||||
|
||||
sendEventFinalize(cmd, respSignal, respProfile, cmd.workflow)
|
||||
success = true
|
||||
|
||||
} else if(cmd.id == 'status' && respSignal != null && respProfile != null) {
|
||||
|
||||
def config = parent.onGetConfig()
|
||||
def workflow = deduceWorkflow(respSignal, respProfile, config)
|
||||
sendEventFinalize(cmd, respSignal, respProfile, workflow)
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
parent.onEndAsyncOp()
|
||||
if(success) {
|
||||
[nextStateId:'success']
|
||||
} else {
|
||||
log.error("getBIfFsmDef[finalize]: error setting status: unsuccessful result from setting status/signal: $params.respMsg.data");
|
||||
sendError("Error setting status/signal: ${respData.data?.reason ?: 'signal mismatch...' }")
|
||||
[nextStateId:'error']
|
||||
}
|
||||
}
|
||||
],
|
||||
timeout: [
|
||||
action: {Map params ->
|
||||
log.error("getBIfFsmDef[timeout]: operation timed out")
|
||||
sendError("Operation timed out...")
|
||||
[nextStateId:'error']
|
||||
}
|
||||
],
|
||||
success: [
|
||||
isFinal:true
|
||||
],
|
||||
error: [
|
||||
isFinal:true
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def configure()
|
||||
{
|
||||
log.debug("configure: reseting states")
|
||||
sendEvent(name:"status", value: ".")
|
||||
sendEvent(name:"state", value: "disarmed") //TODO: initial state calc
|
||||
}
|
||||
|
||||
|
||||
private getBIStg(boolean reset = false)
|
||||
{
|
||||
if (state.biStg == null || reset) {
|
||||
state.biStg = [:]
|
||||
}
|
||||
state.biStg
|
||||
}
|
||||
|
||||
private Map getBICommands()
|
||||
{
|
||||
return [
|
||||
set: {Map p ->
|
||||
def rc = [
|
||||
id: 'set',
|
||||
signal: p.signal,
|
||||
profile: p.profile,
|
||||
workflow: p.workflow //one of ['arm', 'disarm']
|
||||
]
|
||||
if (p.context != null) {
|
||||
rc.context = p.context //optional (location mode name)
|
||||
}
|
||||
|
||||
rc
|
||||
},
|
||||
status: {->
|
||||
return [
|
||||
id: 'status',
|
||||
status: true
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
//signal: null (N/A), true(green), false(red)
|
||||
//profile: null (N/A), number
|
||||
private deduceWorkflow(Boolean signal, String profile, final config)
|
||||
{
|
||||
if ((signal != null && !signal) ||
|
||||
(config.arming?.disarm?.signal == signal && config.arming?.disarm?.profile.toString() == profile)) {
|
||||
|
||||
"disarm"
|
||||
} else {
|
||||
"arm"
|
||||
}
|
||||
}
|
||||
|
||||
def arm()
|
||||
{
|
||||
log.debug('arm: running fsm')
|
||||
|
||||
def config = parent.onGetConfig()
|
||||
log.debug("arm: retrieved config: $config")
|
||||
|
||||
def armConfig = config.arming?.arm
|
||||
if(armConfig) {
|
||||
|
||||
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().set(
|
||||
signal:armConfig.signal, profile:armConfig.profile, workflow:'arm')])
|
||||
|
||||
if(rc.error || !rc.actionResult.isAsync) {
|
||||
log.error("arm: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
|
||||
} else {
|
||||
return rc.actionResult.hubAction
|
||||
}
|
||||
} else {
|
||||
log.error("arm: missing configuration (arming/arm) in $config")
|
||||
// TODO: check return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def disarm()
|
||||
{
|
||||
log.debug('disarm: running fsm')
|
||||
|
||||
def config = parent.onGetConfig();
|
||||
log.debug("disarm: retrieved config: $config")
|
||||
|
||||
def disarmConfig = config.arming?.disarm
|
||||
if(disarmConfig) {
|
||||
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().set(
|
||||
signal:disarmConfig.signal, profile:disarmConfig.profile, workflow:'disarm')])
|
||||
|
||||
if(rc.error || !rc.actionResult.isAsync) {
|
||||
log.error("disarm: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
|
||||
} else {
|
||||
return rc.actionResult.hubAction
|
||||
}
|
||||
} else {
|
||||
log.error("disarm: missing configuration (arming/arm) in $config")
|
||||
// TODO: check return
|
||||
}
|
||||
}
|
||||
|
||||
def location(locationId)
|
||||
{
|
||||
def config = parent.onGetConfig()
|
||||
log.debug("location: retrieved config: $config, processing location $locationId")
|
||||
|
||||
def locationConfig = config.location ? config.location[locationId] : null
|
||||
if(locationConfig) {
|
||||
|
||||
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().set(
|
||||
signal: locationConfig.signal, profile: locationConfig.profile,
|
||||
workflow: deduceWorkflow(locationConfig.signal, locationConfig.profile, config), context: locationConfig.name)])
|
||||
|
||||
if(rc.error || !rc.actionResult.isAsync) {
|
||||
log.error("location: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
|
||||
} else {
|
||||
return rc.actionResult.hubAction
|
||||
}
|
||||
|
||||
} else {
|
||||
log.debug("location: no active configuration found for locationId:${locationId}' in configuration [$config]")
|
||||
// TODO: check return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def retry()
|
||||
{
|
||||
def stg = getBIStg()
|
||||
|
||||
if(stg.cmd != null) {
|
||||
log.debug("retry: running fsm with cmd:$stg.cmd")
|
||||
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:stg.cmd])
|
||||
|
||||
if(rc.error || !rc.actionResult.isAsync) {
|
||||
log.error("retry: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
|
||||
} else {
|
||||
return rc.actionResult.hubAction
|
||||
}
|
||||
} else {
|
||||
log.debug("retry: no state to retry")
|
||||
}
|
||||
}
|
||||
|
||||
def timeout()
|
||||
{
|
||||
def stg = getBIStg()
|
||||
log.debug("timeout: running fsm with cmd:${stg.cmd}")
|
||||
|
||||
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'timeout', cmd:stg.cmd])
|
||||
|
||||
if(rc.error || rc.actionResult.isAsync) {
|
||||
log.error("timeout: error executing fsm: ${rc.error ?: 'expected synchronious action result'}")
|
||||
}
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug('refresh: running fsm: status command')
|
||||
|
||||
def rc = fsmExec(getBIStg(true), getBIfFsmDef(), [fsmInitialState:'init', cmd:getBICommands().status()])
|
||||
|
||||
if(rc.error || !rc.actionResult.isAsync) {
|
||||
log.error("refresh: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
|
||||
} else {
|
||||
return rc.actionResult.hubAction
|
||||
}
|
||||
}
|
||||
|
||||
def parse(msg)
|
||||
{
|
||||
def lanMsg = parseLanMessage(msg)
|
||||
log.info "parse: parsed lan message: $lanMsg"
|
||||
|
||||
|
||||
if (lanMsg && lanMsg.headers && lanMsg.body) {
|
||||
log.info "parse: parsed lan message requestId:$lanMsg.requestId, body:$lanMsg.body"
|
||||
|
||||
def stg = getBIStg()
|
||||
|
||||
if(fsmGetStateId(stg) && stg.cmd.requestId == lanMsg.requestId) {
|
||||
log.debug("parse: received expected response mesage; requestId:$lanMsg.requestId, state:${fsmGetStateId(stg)}")
|
||||
|
||||
def rc = fsmExec(stg, getBIfFsmDef(), [respMsg:lanMsg])
|
||||
|
||||
if(rc.error) {
|
||||
log.error("parse: error executing fsm: ${rc.error ?: 'expected asynchronious action result'}")
|
||||
} else if(rc.actionResult.isAsync) {
|
||||
return [rc.actionResult.hubAction]
|
||||
}
|
||||
|
||||
} else {
|
||||
log.error("parse: skipping message: request id does not match: stg.request_id:$stg.cmd.requestId, lanMsg.requestId:$lanMsg.requestId")
|
||||
}
|
||||
|
||||
} else {
|
||||
log.error("parse: skipping message: unrecognized lan message")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private getOperationTimeoutMs()
|
||||
{
|
||||
10 * 1000;
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private getHostAddress() {
|
||||
def parts = device.deviceNetworkId.split(":")
|
||||
def ip = convertHexToIP(parts[0])
|
||||
def port = convertHexToInt(parts[1])
|
||||
return ip + ":" + port
|
||||
}
|
||||
|
||||
private hashMD5(String somethingToHash) {
|
||||
java.security.MessageDigest.getInstance("MD5").digest(somethingToHash.getBytes("UTF-8")).encodeHex().toString()
|
||||
}
|
||||
|
||||
private calcDigestAuth(String method, String uri) {
|
||||
def HA1 = hashMD5("${getUsername}::${getPassword}")
|
||||
def HA2 = hashMD5("${method}:${uri}")
|
||||
def response = hashMD5("${HA1}::::auth:${HA2}")
|
||||
|
||||
'Digest username="'+ getUsername() + '", realm="", nonce="", uri="'+ uri +'", qop=auth, nc=, cnonce="", response="' + response + '", opaque=""'
|
||||
}
|
||||
2
devicetypes/drzwave/ezmultipli.src/.st-ignore
Normal file
2
devicetypes/drzwave/ezmultipli.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
44
devicetypes/drzwave/ezmultipli.src/README.md
Normal file
44
devicetypes/drzwave/ezmultipli.src/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Express Controls EZMultiPli
|
||||
|
||||
Works with:
|
||||
|
||||
* [Express Controls EZMultiPli](https://www.smartthings.com/works-with-smartthings/)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Release Notes](#release-notes)
|
||||
* [Capabilities](#capabilities)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Release Notes
|
||||
|
||||
* **2017-04-19** - _dkirker_ - Update default config values in config value range check functions, use lux if lum option is null, fix NullPointerException on initial pairing when color data has not been set (and set the default color data!)
|
||||
* **2017-04-10** - _DrZwave_ (with help from Donald Kirker) - changed fingerprint to the new format, lowered the OnTime and other parameters to be "more in line with ST user expectations", get the luminance in LUX so it reports in lux all the time.
|
||||
* **2016-10-06** - _erocm1231_ - Added "updated" method to run when configuration options are changed. Depending on model of unit, luminance is being reported as a relative percentace or as a lux value. Added the option to configure this in the handler.
|
||||
* **2016-01-28** - _erocm1231_ - Changed the configuration method to use scaledConfiguration so that it properly formatted negative numbers. Also, added configurationGet and a configurationReport method so that config values can be verified.
|
||||
* **2015-12-04** - _erocm1231_ - added range value to preferences as suggested by @Dela-Rick.
|
||||
* **2015-11-26** - _erocm1231_ - Fixed null condition error when adding as a new device.
|
||||
* **2015-11-24** - _erocm1231_ - Added refresh command. Made a few changes to how the handler maps colors to the LEDs. Fixed the device not having its on/off status updated when colors are changed.
|
||||
* **2015-11-23** - _erocm1231_ - Changed the look to match SmartThings v2 devices.
|
||||
* **2015-11-21** - _erocm1231_ - Made code much more efficient. Also made it compatible when setColor is passed a hex value. Mapping of special colors: Soft White - Default - Yellow, White - Concentrate - White, Daylight - Energize - Teal, Warm White - Relax - Yellow
|
||||
* **2015-11-19** - _erocm1231_ - Fixed a couple incorrect colors, changed setColor to be more compatible with other apps
|
||||
* **2015-11-18** - _erocm1231_ - Added to setColor for compatibility with Smart Lighting
|
||||
* **v0.1.0** - _DrZWave_ - chose better icons, Got color LED to work - first fully functional version
|
||||
* **v0.0.9** - _jrs_ - got the temp and luminance to work. Motion works. Debugging the color wheel.
|
||||
* **v0.0.8** - _DrZWave_ 2/25/2015 - change the color control to be tiles since there are only 8 colors.
|
||||
* **v0.0.7** - _jrs_ - 02/23/2015 - Jim Sulin
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - represents that a Device has commands
|
||||
* **Sensor** - detects sensor events
|
||||
* **Motion Sensor** - can detect motion
|
||||
* **Temperature Measurement** - defines device measures current temperature
|
||||
* **Illuminance Measurement** - gives the illuminance reading from devices that support it
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Color Control** - represents that the color attributes of a device can be controlled (hue, saturation, color value
|
||||
* **Configuration** - configure() command called when device is installed or device preferences updated
|
||||
* **Refresh** - refresh() command for status updates
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -2,27 +2,6 @@
|
||||
// Motion Sensor - Temperature - Light level - 8 Color Indicator LED - Z-Wave Range Extender - Wall Powered
|
||||
// driver for SmartThings
|
||||
// The EZMultiPli is also known as the HSM200 from HomeSeer.com
|
||||
//
|
||||
// 2017-04-10 - DrZwave (with help from Don Kirker) - changed fingerprint to the new format, lowered the OnTime
|
||||
// and other parameters to be "more in line with ST user expectations", get the luminance in LUX so it reports in lux all the time.
|
||||
// 2016-10-06 - erocm1231 - Added "updated" method to run when configuration options are changed. Depending on model of unit, luminance is being
|
||||
// reported as a relative percentace or as a lux value. Added the option to configure this in the handler.
|
||||
// 2016-01-28 - erocm1231 - Changed the configuration method to use scaledConfiguration so that it properly formatted negative numbers.
|
||||
// Also, added configurationGet and a configurationReport method so that config values can be verified.
|
||||
// 2015-12-04 - erocm1231 - added range value to preferences as suggested by @Dela-Rick.
|
||||
// 2015-11-26 - erocm1231 - Fixed null condition error when adding as a new device.
|
||||
// 2015-11-24 - erocm1231 - Added refresh command. Made a few changes to how the handler maps colors to the LEDs. Fixed
|
||||
// the device not having its on/off status updated when colors are changed.
|
||||
// 2015-11-23 - erocm1231 - Changed the look to match SmartThings v2 devices.
|
||||
// 2015-11-21 - erocm1231 - Made code much more efficient. Also made it compatible when setColor is passed a hex value.
|
||||
// Mapping of special colors: Soft White - Default - Yellow, White - Concentrate - White,
|
||||
// Daylight - Energize - Teal, Warm White - Relax - Yellow
|
||||
// 2015-11-19 - erocm1231 - Fixed a couple incorrect colors, changed setColor to be more compatible with other apps
|
||||
// 2015-11-18 - erocm1231 - Added to setColor for compatibility with Smart Lighting
|
||||
// v0.1.0 - DrZWave - chose better icons, Got color LED to work - first fully functional version
|
||||
// v0.0.9 - jrs - got the temp and luminance to work. Motion works. Debugging the color wheel.
|
||||
// v0.0.8 - DrZWave 2/25/2015 - change the color control to be tiles since there are only 8 colors.
|
||||
// v0.0.7 - jrs - 02/23/2015 - Jim Sulin
|
||||
|
||||
metadata {
|
||||
definition (name: "EZmultiPli", namespace: "DrZWave", author: "Eric Ryherd", oauth: true) {
|
||||
@@ -131,7 +110,6 @@ metadata {
|
||||
|
||||
} // end metadata
|
||||
|
||||
|
||||
// Parse incoming device messages from device to generate events
|
||||
def parse(String description){
|
||||
//log.debug "==> New Zwave Event: ${description}"
|
||||
@@ -145,7 +123,7 @@ def parse(String description){
|
||||
|
||||
def statusTextmsg = ""
|
||||
if (device.currentState('temperature') != null && device.currentState('illuminance') != null) {
|
||||
statusTextmsg = "${device.currentState('temperature').value} ° - ${device.currentState('illuminance').value} ${(lum == "" || lum == null || lum == 1) ? "%" : "LUX"}"
|
||||
statusTextmsg = "${device.currentState('temperature').value} ° - ${device.currentState('illuminance').value} ${(lum == 1) ? "%" : "LUX"}"
|
||||
sendEvent("name":"statusText", "value":statusTextmsg, displayed:false)
|
||||
}
|
||||
if (result != [null] && result != []) log.debug "Parse returned ${result}"
|
||||
@@ -168,7 +146,7 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR
|
||||
break;
|
||||
case 0x03 : // SENSOR_TYPE_LUMINANCE_VERSION_1
|
||||
map.value = cmd.scaledSensorValue.toInteger().toString()
|
||||
if(lum == "" || lum == null || lum == 1) map.unit = "%"
|
||||
if(lum == 1) map.unit = "%"
|
||||
else map.unit = "lux"
|
||||
map.name = "illuminance"
|
||||
log.debug "Luminance report"
|
||||
@@ -203,7 +181,8 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
if (cmd.value == 0 && device.latestState("color").value != "#ffffff") {
|
||||
// The EZMultiPli sets the color back to #ffffff on "off" or at init, so update the ST device to reflect this.
|
||||
if (device.latestState("color") == null || (cmd.value == 0 && device.latestState("color").value != "#ffffff")) {
|
||||
sendEvent(name: "color", value: "#ffffff", displayed: true)
|
||||
}
|
||||
[name: "switch", value: cmd.value ? "on" : "off", type: "digital"]
|
||||
@@ -296,12 +275,12 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
// ensure we are passing acceptable param values for LiteMin & TempMin configs
|
||||
def checkLiteTempInput(value) {
|
||||
if (value == null) {
|
||||
value=60
|
||||
value=6
|
||||
}
|
||||
def liteTempVal = value.toInteger()
|
||||
switch (liteTempVal) {
|
||||
case { it < 0 }:
|
||||
return 60 // bad value, set to default
|
||||
return 6 // bad value, set to default
|
||||
break
|
||||
case { it > 127 }:
|
||||
return 127 // bad value, greater then MAX, set to MAX
|
||||
@@ -314,12 +293,12 @@ def checkLiteTempInput(value) {
|
||||
// ensure we are passing acceptable param value for OnTime config
|
||||
def checkOnTimeInput(value) {
|
||||
if (value == null) {
|
||||
value=10
|
||||
value=2
|
||||
}
|
||||
def onTimeVal = value.toInteger()
|
||||
switch (onTimeVal) {
|
||||
case { it < 0 }:
|
||||
return 10 // bad value set to default
|
||||
return 2 // bad value set to default
|
||||
break
|
||||
case { it > 127 }:
|
||||
return 127 // bad value, greater then MAX, set to MAX
|
||||
|
||||
2
devicetypes/smartthings/arrival-sensor-ha.src/.st-ignore
Normal file
2
devicetypes/smartthings/arrival-sensor-ha.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
50
devicetypes/smartthings/arrival-sensor-ha.src/README.md
Normal file
50
devicetypes/smartthings/arrival-sensor-ha.src/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Arrival Sensor HA (2016+ Model)
|
||||
|
||||
Cloud Execution
|
||||
|
||||
Works with:
|
||||
|
||||
* [Samsung SmartThings Arrival Sensor](https://support.smartthings.com/hc/en-us/articles/212417083-Samsung-SmartThings-Arrival-Sensor)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Battery](#battery)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Tone** - beep command to allow an audible tone
|
||||
* **Actuator** - device has commands
|
||||
* **Presence Sensor** - device tells presence with enum - {present, not present}
|
||||
* **Sensor** - device has attributes
|
||||
* **Battery** - defines device uses a battery
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
|
||||
## Device Health
|
||||
|
||||
Arrival Sensor ZigBee is an untracked device. Sends broadcast of battery level every 20 seconds.
|
||||
Disconnects when Hub goes OFFLINE.
|
||||
|
||||
|
||||
## Battery
|
||||
|
||||
Uses 1 CR2032 Battery
|
||||
|
||||
* [Changing the Battery](https://support.smartthings.com/hc/en-us/articles/200907400-How-to-change-the-battery-in-the-SmartSense-Presence-Sensor-and-Samsung-SmartThings-Arrival-Sensor)
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the arrival sensor is out of range.
|
||||
Pairing needs to be tried again by placing the sensor closer to the hub.
|
||||
|
||||
* [Samsung SmartThings Arrival Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205382134-Samsung-SmartThings-Arrival-Sensor-2015-model-)
|
||||
|
||||
If the arrival sensor doesn't update its status, here are a few things you can try to debug.
|
||||
|
||||
* [Troubleshooting: Samsung SmartThings Arrival Sensor won't update its status](https://support.smartthings.com/hc/en-us/articles/200846514-Troubleshooting-Samsung-SmartThings-Arrival-Sensor-won-t-update-its-status)
|
||||
@@ -1,3 +1,5 @@
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
*
|
||||
@@ -19,6 +21,7 @@ metadata {
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020", outClusters: "0003,0019",
|
||||
manufacturer: "SmartThings", model: "tagv4", deviceJoinName: "Arrival Sensor"
|
||||
@@ -58,6 +61,11 @@ def updated() {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// Arrival sensors only goes OFFLINE when Hub is off
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
def cmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + zigbee.batteryConfig(20, 20, 0x01)
|
||||
log.debug "configure -- cmds: ${cmds}"
|
||||
|
||||
2
devicetypes/smartthings/arrival-sensor.src/.st-ignore
Normal file
2
devicetypes/smartthings/arrival-sensor.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
49
devicetypes/smartthings/arrival-sensor.src/README.md
Normal file
49
devicetypes/smartthings/arrival-sensor.src/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Arrival Sensor (2015 Model)
|
||||
|
||||
Cloud Execution
|
||||
|
||||
Works with:
|
||||
|
||||
* [Arrival Sensor](https://www.smartthings.com/products/samsung-smartthings-arrival-sensor)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Battery](#battery)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Tone** - beep command to allow an audible tone
|
||||
* **Actuator** - device has commands
|
||||
* **Signal Strength** - device can read the strength of signal- lqi: Link Quality Indication, rssi: Received Signal Strength Indication
|
||||
* **Presence Sensor** - device tells presence with enum - {present, not present}
|
||||
* **Sensor** - device has attributes
|
||||
* **Battery** - defines device uses a battery
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
|
||||
## Device Health
|
||||
|
||||
Arrival Sensor ZigBee is an untracked device. Disconnects when Hub goes OFFLINE.
|
||||
|
||||
|
||||
## Battery
|
||||
|
||||
Uses 1 CR2032 Battery
|
||||
|
||||
* [Changing the Battery](https://support.smartthings.com/hc/en-us/articles/200907400-How-to-change-the-battery-in-the-SmartSense-Presence-Sensor-and-Samsung-SmartThings-Arrival-Sensor)
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the arrival sensor is out of range.
|
||||
Pairing needs to be tried again by placing the sensor closer to the hub.
|
||||
|
||||
* [Samsung SmartThings Arrival Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205382134-Samsung-SmartThings-Arrival-Sensor-2015-model-)
|
||||
|
||||
If the arrival sensor doesn't update its status, here are a few things you can try to debug.
|
||||
|
||||
* [Troubleshooting: Samsung SmartThings Arrival Sensor won't update its status](https://support.smartthings.com/hc/en-us/articles/200846514-Troubleshooting-Samsung-SmartThings-Arrival-Sensor-won-t-update-its-status)
|
||||
@@ -1,3 +1,5 @@
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
@@ -19,6 +21,7 @@ metadata {
|
||||
capability "Presence Sensor"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint profileId: "FC01", deviceId: "019A"
|
||||
fingerprint profileId: "FC01", deviceId: "0131", inClusters: "0000,0003", outClusters: "0003"
|
||||
@@ -111,6 +114,11 @@ def beep() {
|
||||
]
|
||||
}
|
||||
|
||||
def installed() {
|
||||
// Arrival sensors only goes OFFLINE when Hub is off
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def results
|
||||
if (isBatteryMessage(description)) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Dimmer Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch") {
|
||||
definition (name: "Dimmer Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Indicator"
|
||||
|
||||
@@ -33,7 +33,6 @@ metadata {
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3323-G", deviceJoinName: "Centralite Micro Door Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
|
||||
|
||||
attribute "status", "string"
|
||||
|
||||
@@ -31,6 +31,7 @@ metadata {
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
|
||||
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3320-L", deviceJoinName: "Iris Contact Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3323-G", deviceJoinName: "Centralite Micro Door Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Z-Wave Metering Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch") {
|
||||
definition (name: "Z-Wave Metering Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") {
|
||||
capability "Switch"
|
||||
capability "Polling"
|
||||
capability "Power Meter"
|
||||
|
||||
@@ -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 (LocalConnect2)
|
||||
*
|
||||
* Author: Nicolas Neverov
|
||||
* Date: 2017-04-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);
|
||||
}
|
||||
@@ -136,10 +136,20 @@ def getDataForChild(child, startDate, endDate) {
|
||||
|
||||
def wattvisionURL = wattvisionURL(child.deviceNetworkId, startDate, endDate)
|
||||
if (wattvisionURL) {
|
||||
httpGet(uri: wattvisionURL) { response ->
|
||||
def json = new org.json.JSONObject(response.data.toString())
|
||||
child.addWattvisionData(json)
|
||||
return "success"
|
||||
try {
|
||||
httpGet(uri: wattvisionURL) { response ->
|
||||
def json = new org.json.JSONObject(response.data.toString())
|
||||
child.addWattvisionData(json)
|
||||
return "success"
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException httpE) {
|
||||
log.error "Wattvision getDataForChild HttpResponseException: ${httpE} -> ${httpE.response.data}"
|
||||
//log.debug "wattvisionURL = ${wattvisionURL}"
|
||||
return "fail"
|
||||
} catch (e) {
|
||||
log.error "Wattvision getDataForChild General Exception: ${e}"
|
||||
//log.debug "wattvisionURL = ${wattvisionURL}"
|
||||
return "fail"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,9 +174,14 @@ def wattvisionURL(senorId, startDate, endDate) {
|
||||
if (diff > 259200000) { // 3 days in milliseconds
|
||||
// Wattvision only allows pulling 3 hours of data at a time
|
||||
startDate = new Date(hours: endDate.hours - 3)
|
||||
} else if (diff < 10000) { // 10 seconds in milliseconds
|
||||
// Wattvision throws errors when the difference between start_time and end_time is 5 seconds or less
|
||||
// So we are going to make sure that we have a few more seconds of breathing room
|
||||
use (groovy.time.TimeCategory) {
|
||||
startDate = endDate - 10.seconds
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def params = [
|
||||
"sensor_id" : senorId,
|
||||
"api_id" : wattvisionApiAccess.id,
|
||||
@@ -480,4 +495,3 @@ def connectionSuccessful(deviceName, iconSrc) {
|
||||
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user