mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-14 05:11:50 +00:00
Compare commits
2 Commits
MSA-1956-3
...
MSA-1939-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04e098603d | ||
|
|
7b683677d1 |
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=""'
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
* Copyright 2016 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:
|
||||
@@ -64,7 +64,6 @@ private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
@@ -85,11 +84,11 @@ def parse(String description) {
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||
}
|
||||
}
|
||||
@@ -124,12 +123,7 @@ def ping() {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.levelRefresh() +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +
|
||||
zigbee.onOffConfig(0, 300) +
|
||||
zigbee.levelConfig()
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
@@ -139,38 +133,26 @@ def configure() {
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
refresh()
|
||||
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
private getScaledHue(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
private getScaledSaturation(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() +
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND,
|
||||
getScaledHue(value.hue), getScaledSaturation(value.saturation), "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
zigbee.on() + setHue(value.hue) + "delay 500" + setSaturation(value.saturation)
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE)
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def installed() {
|
||||
@@ -179,4 +161,4 @@ def installed() {
|
||||
sendEvent(name: "level", value: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
* Copyright 2016 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:
|
||||
@@ -78,7 +78,6 @@ private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
@@ -103,11 +102,11 @@ def parse(String description) {
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||
}
|
||||
}
|
||||
@@ -142,13 +141,7 @@ def ping() {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.levelRefresh() +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +
|
||||
zigbee.onOffConfig(0, 300) +
|
||||
zigbee.levelConfig()
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
@@ -163,12 +156,7 @@ def configure() {
|
||||
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
value = value as Integer
|
||||
def tempInMired = (1000000 / value) as Integer
|
||||
def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4))
|
||||
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, 0x0A, "$finalHex 0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE)
|
||||
zigbee.setColorTemperature(value)
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
@@ -192,31 +180,19 @@ def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
private getScaledHue(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
private getScaledSaturation(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() +
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND,
|
||||
getScaledHue(value.hue), getScaledSaturation(value.saturation), "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
zigbee.on() + setHue(value.hue) + "delay 300" + setSaturation(value.saturation)
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE)
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def installed() {
|
||||
@@ -225,4 +201,4 @@ def installed() {
|
||||
sendEvent(name: "level", value: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
* 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:
|
||||
@@ -71,11 +71,6 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// Globals
|
||||
private getMOVE_TO_COLOR_TEMPERATURE_COMMAND() { 0x0A }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
@@ -128,11 +123,7 @@ def ping() {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.levelRefresh() +
|
||||
zigbee.colorTemperatureRefresh() +
|
||||
zigbee.onOffConfig(0, 300) +
|
||||
zigbee.levelConfig()
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
@@ -147,12 +138,7 @@ def configure() {
|
||||
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
value = value as Integer
|
||||
def tempInMired = (1000000 / value) as Integer
|
||||
def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4))
|
||||
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE)
|
||||
zigbee.setColorTemperature(value)
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
* Copyright 2016 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:
|
||||
@@ -55,7 +55,6 @@ private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
@@ -73,11 +72,11 @@ def parse(String description) {
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
@@ -109,46 +108,28 @@ def configure() {
|
||||
}
|
||||
|
||||
def configureAttributes() {
|
||||
zigbee.onOffConfig() +
|
||||
zigbee.levelConfig()
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def refreshAttributes() {
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.levelRefresh() +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||
}
|
||||
|
||||
private getScaledHue(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
private getScaledSaturation(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() +
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND,
|
||||
getScaledHue(value.hue), getScaledSaturation(value.saturation), "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes()
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
//payload-> hue value, direction (00-> shortest distance), transition time (1/10th second)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE)
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
//payload-> sat value, transition time
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
* Copyright 2016 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:
|
||||
@@ -70,7 +70,6 @@ private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
@@ -89,11 +88,11 @@ def parse(String description) {
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100)
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
@@ -125,16 +124,11 @@ def configure() {
|
||||
}
|
||||
|
||||
def configureAttributes() {
|
||||
zigbee.onOffConfig() +
|
||||
zigbee.levelConfig()
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT8, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT8, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def refreshAttributes() {
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.levelRefresh() +
|
||||
zigbee.colorTemperatureRefresh() +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
@@ -145,32 +139,17 @@ def setLevel(value) {
|
||||
zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||
}
|
||||
|
||||
private getScaledHue(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
private getScaledSaturation(value) {
|
||||
zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() +
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND,
|
||||
getScaledHue(value.hue), getScaledSaturation(value.saturation), "0000") +
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes()
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
//payload-> hue value, direction (00-> shortest distance), transition time (1/10th second)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE)
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
//payload-> sat value, transition time
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
* Copyright 2016 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:
|
||||
@@ -66,11 +66,6 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// Globals
|
||||
private getMOVE_TO_COLOR_TEMPERATURE_COMMAND() { 0x0A }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
@@ -100,14 +95,14 @@ def setLevel(value) {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
|
||||
// Do NOT config if the device is the Eaton Halo_LT01, it responds with "switch:off" to onOffConfig, and maybe other weird things with the others
|
||||
// Do NOT config if the device is the Eaton Halo_LT01, it responds with "switch:off" to onOffConfig, and maybe other weird things with the others
|
||||
if (!((device.getDataValue("manufacturer") == "Eaton") && (device.getDataValue("model") == "Halo_LT01"))) {
|
||||
cmds += zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
cmds = cmds + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
}
|
||||
|
||||
cmds
|
||||
cmds
|
||||
}
|
||||
|
||||
def poll() {
|
||||
@@ -143,7 +138,7 @@ def configure() {
|
||||
log.debug "configure()"
|
||||
configureHealthCheck()
|
||||
// Implementation note: for the Eaton Halo_LT01, it responds with "switch:off" to onOffConfig, so be sure this is before the call to onOffRefresh
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
@@ -153,12 +148,7 @@ def updated() {
|
||||
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
value = value as Integer
|
||||
def tempInMired = (1000000 / value) as Integer
|
||||
def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4))
|
||||
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE)
|
||||
zigbee.setColorTemperature(value) + ["delay 1500"] + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2017 SmartThings
|
||||
* Copyright 2016 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:
|
||||
@@ -68,11 +68,6 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// Globals
|
||||
private getMOVE_TO_COLOR_TEMPERATURE_COMMAND() { 0x0A }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
@@ -99,11 +94,7 @@ def setLevel(value) {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.levelRefresh() +
|
||||
zigbee.colorTemperatureRefresh() +
|
||||
zigbee.onOffConfig() +
|
||||
zigbee.levelConfig()
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
@@ -138,7 +129,8 @@ def configureHealthCheck() {
|
||||
def configure() {
|
||||
log.debug "configure()"
|
||||
configureHealthCheck()
|
||||
refresh()
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
@@ -148,12 +140,7 @@ def updated() {
|
||||
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
value = value as Integer
|
||||
def tempInMired = (1000000 / value) as Integer
|
||||
def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4))
|
||||
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") +
|
||||
zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE)
|
||||
zigbee.setColorTemperature(value) + ["delay 1500"] + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
/**
|
||||
* Lloyds Banking Group Connect & Protect
|
||||
*
|
||||
* Copyright 2016 Domotz
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Lloyds Banking Group Connect & Protect",
|
||||
namespace: "domotz.dev",
|
||||
author: "Domotz",
|
||||
description: "The Lloyds Connect & Protect SmartApp is a bridge between SmartThings Cloud and Lloyds Banking Group to enable advanced connected device monitoring and alerting features to be included in the offering to their customers for the connected home service",
|
||||
category: "Convenience",
|
||||
singleInstance: true,
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
oauth: [displayName: "Lloyds Banking Group Connect & Protect", displayLink: ""]) {
|
||||
appSetting "endpointRetrievalUrl"
|
||||
appSetting "xApiKey"
|
||||
}
|
||||
|
||||
def getSupportedTypes() {
|
||||
return [
|
||||
[obj: switches, name: "switches", attribute: "switch", capability: "switch", title: "Switches"],
|
||||
[obj: motions, name: "motions", attribute: "motion", capability: "motionSensor", title: "Motion Sensors"],
|
||||
[obj: temperature, name: "temperature", attribute: "temperature", capability: "temperatureMeasurement", title: "Temperature Sensors"],
|
||||
[obj: contact, name: "contact", attribute: "contact", capability: "contactSensor", title: "Contact Sensors"],
|
||||
[obj: presence, name: "presence", attribute: "presence", capability: "presenceSensor", title: "Presence Sensors"],
|
||||
[obj: water, name: "water", attribute: "water", capability: "waterSensor", title: "Water Sensors"],
|
||||
[obj: smoke, name: "smoke", attribute: "smoke", capability: "smokeDetector", title: "Smoke Sensors"],
|
||||
[obj: battery, name: "battery", attribute: "battery", capability: "battery", title: "Batteries"]
|
||||
]
|
||||
}
|
||||
|
||||
preferences {
|
||||
section("Allow Lloyds Banking Group Connect & Protect service to monitor these devices") {
|
||||
for (type in getSupportedTypes()) {
|
||||
input type.get("name"), "capability.${type.get('capability')}", title: type.get('title'), multiple: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
initialize()
|
||||
hubUpdateHandler()
|
||||
|
||||
}
|
||||
|
||||
def getRequestHeaders() {
|
||||
return ['Accept': '*/*', 'X-API-KEY': appSettings.xApiKey]
|
||||
}
|
||||
|
||||
def subscribeToDeviceEvents() {
|
||||
for (type in getSupportedTypes()) {
|
||||
subscribe(type.get("obj"), "${type.get("attribute")}", genericDeviceEventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
if (atomicState.endpoint != null) {
|
||||
log.debug "Detected endpoint: ${atomicState.endpoint}"
|
||||
subscribeToDeviceEvents()
|
||||
subscribe(location, "routineExecuted", modeChangeHandler)
|
||||
subscribe(location, "mode", modeChangeHandler)
|
||||
} else {
|
||||
log.debug "There is no endpoint, requesting domotz for a new one"
|
||||
requestNewEndpoint()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def getHubLocation() {
|
||||
def location_info = [:]
|
||||
location_info['uid'] = location.id
|
||||
//location_info['hubs'] = location.hubs
|
||||
location_info['latitude'] = location.latitude
|
||||
location_info['longitude'] = location.longitude
|
||||
location_info['current_mode'] = location.mode
|
||||
//location_info['modes'] = location.modes
|
||||
location_info['name'] = location.name
|
||||
location_info['temperature_scale'] = location.temperatureScale
|
||||
location_info['version'] = location.version
|
||||
location_info['channel_name'] = location.channelName
|
||||
location_info['zip_code'] = location.zipCode
|
||||
log.debug "Triggered getHubLocation with properties: ${location_info}"
|
||||
return location_info
|
||||
}
|
||||
|
||||
def modeChangeHandler(evt) {
|
||||
log.debug "mode changed to ${evt.value}"
|
||||
def url = null
|
||||
if (atomicState.endpoint != null) {
|
||||
url = atomicState.endpoint + '/hub-change'
|
||||
|
||||
httpPutJson(
|
||||
uri: url,
|
||||
body: getHubLocation(),
|
||||
headers: getRequestHeaders()
|
||||
)
|
||||
} else {
|
||||
log.debug "There is no endpoint, requesting domotz for a new one"
|
||||
requestNewEndpoint()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def genericDeviceEventHandler(event) {
|
||||
log.debug "Device Event Handler, event properties: ${event.getProperties().toString()}"
|
||||
log.debug "Device Event Handler, value: ${event.value}"
|
||||
def resp = [:]
|
||||
def url = null
|
||||
def device = null
|
||||
|
||||
device = getDevice(event.device, resp)
|
||||
|
||||
url = atomicState.endpoint + "/device/" + device.provider_uid + "/${event.name}"
|
||||
|
||||
log.debug "Device Event Handler, put url: ${url}"
|
||||
log.debug "Device Event Handler, put body: value: ${event.value}\ndate: ${event.isoDate}"
|
||||
httpPutJson(
|
||||
uri: url,
|
||||
body: [
|
||||
"value": event.value,
|
||||
"time" : event.isoDate
|
||||
],
|
||||
headers: getRequestHeaders()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def hubUpdateHandler() {
|
||||
log.debug "Hub Update Handler, with settings: ${settings}"
|
||||
def url = null
|
||||
def deviceList = [:]
|
||||
|
||||
if (atomicState.endpoint != null) {
|
||||
url = atomicState.endpoint + '/device-list'
|
||||
log.debug "Hub Update Event Handler, put url: ${url}"
|
||||
deviceList = getDeviceList()
|
||||
httpPutJson(
|
||||
uri: url,
|
||||
body: deviceList,
|
||||
headers: getRequestHeaders()
|
||||
)
|
||||
} else {
|
||||
log.debug "There is no endpoint, requesting domotz for a new one"
|
||||
requestNewEndpoint()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
def getDeviceList() {
|
||||
try {
|
||||
|
||||
def resp = [:]
|
||||
def attribute = null
|
||||
|
||||
for (type in getSupportedTypes()) {
|
||||
type.get("obj").each {
|
||||
device = getDevice(it, resp)
|
||||
attribute = type.get("attribute")
|
||||
if (it.currentState(attribute)) {
|
||||
device['attributes'][attribute] = [
|
||||
"value": it.currentState(attribute).value,
|
||||
"time" : it.currentState(attribute).getIsoDate(),
|
||||
"unit" : it.currentState(attribute).unit
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return resp
|
||||
} catch (e) {
|
||||
log.debug("caught exception", e)
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
|
||||
def getDevice(it, resp) {
|
||||
if (resp[it.id]) {
|
||||
return resp[it.id]
|
||||
}
|
||||
resp[it.id] = [name: it.name, display_name: it.displayName, provider_uid: it.id, type: it.typeName, label: it.label, manufacturer_name: it.manufacturerName, model: it.modelName, attributes: [:]]
|
||||
|
||||
}
|
||||
|
||||
def activateMonitoring(resp) {
|
||||
unsubscribe()
|
||||
log.debug "Event monitoring activated for endpoint: ${request.JSON.endpoint}"
|
||||
atomicState.endpoint = request.JSON.endpoint
|
||||
log.debug "Event monitoring activated for endpoint: ${atomicState.endpoint}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def deactivateMonitoring() {
|
||||
log.debug "Event monitoring deactivated."
|
||||
atomicState.endpoint = null
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
def requestNewEndpoint() {
|
||||
log.debug "Requesting a new endpoint."
|
||||
def hubId = location.id
|
||||
def params = [
|
||||
uri : "${appSettings.endpointRetrievalUrl}/${hubId}/endpoint",
|
||||
headers: getRequestHeaders()
|
||||
]
|
||||
|
||||
try {
|
||||
httpGet(params) { response ->
|
||||
log.debug "Request was successful, received endpoint: ${response.data.endpoint}"
|
||||
atomicState.endpoint = response.data.endpoint
|
||||
subscribeToDeviceEvents()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
log.debug "Unable to retrieve the endpoint"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def handleClientUninstall() {
|
||||
log.info("Deactivated from client")
|
||||
try {
|
||||
app.delete()
|
||||
} catch (e) {
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
httpError(500, "An error occurred during deleting SmartApp: ${e}")
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/device") {
|
||||
action:
|
||||
[
|
||||
GET: getDeviceList
|
||||
]
|
||||
}
|
||||
path("/location") {
|
||||
action:
|
||||
[
|
||||
GET: getHubLocation
|
||||
]
|
||||
}
|
||||
path("/monitoring") {
|
||||
action:
|
||||
[
|
||||
POST : activateMonitoring,
|
||||
DELETE: deactivateMonitoring
|
||||
]
|
||||
}
|
||||
path("/uninstall") {
|
||||
action
|
||||
[GET: handleClientUninstall]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user