Compare commits

..

2 Commits

Author SHA1 Message Date
Nicolas Neverov
04e098603d Modifying 'Blue Iris Integration intranet' 2017-04-30 06:11:00 -07:00
Nicolas Neverov
7b683677d1 MSA-1939: BlueIris is a well known security camera management software.
This App+Handler bundle allows to integrate it with Samsung SmartThings, allowing to to manage Blue Iris signal and profile state via SmartThings app - either manually or automatically, by subscribing to location events (like Home, Away, etc).
The main difference with existing integration (by @pursual) is that this one works locally, thus eliminating need to create a potential security breach by exposing ports to internet (it works without any port forwarding). Of course it means BlueIris server should reside on the same network segment as SmartThings Hub.
2017-04-30 06:07:00 -07:00
13 changed files with 1007 additions and 731 deletions

View 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=""'
}

View File

@@ -103,7 +103,7 @@ metadata {
}
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
state "illuminance", label:'${currentValue} lux', unit:""
state "illuminance", label:'${currentValue} ${unit}', unit:"lux"
}
valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) {
@@ -410,4 +410,4 @@ private command(physicalgraph.zwave.Command cmd) {
private commands(commands, delay=200) {
log.info "sending commands: ${commands}"
delayBetween(commands.collect{ command(it) }, delay)
}
}

View File

@@ -86,7 +86,7 @@ metadata {
state "humidity", label:'${currentValue}% humidity', unit:""
}
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
state "luminosity", label:'${currentValue} lux', unit:""
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:""
@@ -282,4 +282,5 @@ private secure(physicalgraph.zwave.Command cmd) {
private secureSequence(commands, delay=200) {
delayBetween(commands.collect{ secure(it) }, delay)
}
}

View File

@@ -79,7 +79,7 @@ metadata {
state "humidity", label:'${currentValue}% humidity', unit:""
}
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
state "luminosity", label:'${currentValue} lux', unit:""
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
}
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:""
@@ -193,4 +193,4 @@ def configure() {
// set data reporting period to 5 minutes
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format()
])
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -1,549 +0,0 @@
/**
* Copyright 2017 Stelpro
*
* 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.
*
* Stelpro Ki Thermostat
*
* Author: Stelpro
*
* Date: 2017-05-08
*/
preferences {
input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "[Do not use space](Blank = No Forecast)")
input("heatdetails", "enum", title: "Do you want a detailed operating state notification?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: true)
}
metadata {
definition (name: "Stelpro Ki Thermostat", namespace: "stelpro", author: "Stelpro") {
capability "Thermostat"
capability "Temperature Measurement"
capability "Actuator"
capability "Polling"
capability "Refresh"
capability "Sensor"
capability "Configuration"
attribute "outsideTemp", "number"
command "switchMode"
command "quickSetHeat"
command "quickSetOutTemp"
command "increaseHeatSetpoint"
command "decreaseHeatSetpoint"
command "setCustomThermostatMode"
command "eco"
command "applyNow"
fingerprint deviceId: "0x0806", inClusters: "0x5E,0x86,0x72,0x40,0x43,0x31,0x85,0x59,0x5A,0x73,0x20,0x42"
}
// simulator metadata
simulator {
//Add test code here
}
tiles {
multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) {
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
attributeState("temp", label:'${currentValue}')
attributeState("high", label:'HIGH')
attributeState("low", label:'LOW')
attributeState("--", label:'--')
}
tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") {
attributeState("VALUE_UP", action: "increaseHeatSetpoint")
attributeState("VALUE_DOWN", action: "decreaseHeatSetpoint")
}
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
attributeState("idle", backgroundColor:"#44b621")
attributeState("heating", backgroundColor:"#ffa81e")
}
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
attributeState("off", label:'Off')
attributeState("comfort", label:'Comfort')
attributeState("eco", label:'Eco')
}
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT")
{
attributeState("heatingSetpoint", label:'${currentValue}')
}
}
standardTile("mode", "device.thermostatMode", width: 2, height: 2) {
state "off", label:'${name}', action:"switchMode", nextState:"to_comfort", icon:"st.thermostat.off"
state "comfort", label:'${name}', action:"switchMode", nextState:"to_eco", icon:"http://cdn.device-icons.smartthings.com/Home/home29-icn@2x.png"
state "eco", label:'${name}', action:"switchMode", nextState:"...", icon:"http://cdn.device-icons.smartthings.com/Outdoor/outdoor3-icn@2x.png"
state "to_comfort", label: "comfort", action:"switchMode", nextState:"to_eco"
state "to_eco", label: "eco", action:"switchMode", nextState:"..."
state "...", label: "...", action:"heat", nextState:"comfort"
}
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2) {
state "temperature", label:'Setpoint\n${currentValue}°', backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
state "--", label:'--', backgroundColor:"#bdbdbd"
}
standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main ("thermostatMulti")
details(["thermostatMulti", "mode", "heatingSetpoint", "refresh"])
}
}
def parse(String description)
{
if (description == "updated")
return []
def unitScale = getTemperatureScale()
//Class, version
def map = createEvent(zwaveEvent(zwave.parse(description, [0x40:2, 0x43:2, 0x31:3, 0x42:1])))
if (!map) {
return null
}
def result = [map]
if (map.name in ["heatingSetpoint","thermostatMode"]) {
def map2 = [
name: "thermostatSetpoint",
unit: getTemperatureScale()
]
if (map.name == "thermostatMode") {
state.lastTriedMode = map.value
}
else {
def mode = device.latestValue("thermostatMode")
log.info "THERMOSTAT, latest mode = ${mode}"
if (map.name == "heatingSetpoint") {
map2.value = map.value
map2.unit = map.unit
}
}
if (map2.value != null) {
log.debug "THERMOSTAT, adding setpoint event: $map"
result << createEvent(map2)
}
}
log.debug "Parse returned $result"
result
}
// Event Generation
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd)
{
def cmdScale = cmd.scale == 1 ? "F" : "C"
def temp;
float tempfloat;
def map = [:]
if (cmd.scaledValue >= 327)
{
map.value = "--"
}
else
{
temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision)
tempfloat = (Math.round(temp.toFloat() * 2)) / 2
map.value = tempfloat
}
map.unit = getTemperatureScale()
map.displayed = false
switch (cmd.setpointType) {
case 1:
map.name = "heatingSetpoint"
break;
default:
return [:]
}
// So we can respond with same format
state.size = cmd.size
state.scale = cmd.scale
state.precision = cmd.precision
sendEvent(name:"heatingSetpoint", value:map.value)
map
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd)
{
def temp;
float tempfloat;
def format;
def map = [:]
if (cmd.sensorType == 1) {
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision)
map.unit = getTemperatureScale()
map.name = "temperature"
temp = map.value
if (temp == "32765") //0x7FFD
{
map.value = "low"
}
else if (temp == "32767") //0x7FFF
{
map.value = "high"
}
else if (temp == "-32768") //0x8000
{
map.value = "--"
}
else
{
tempfloat = (Math.round(temp.toFloat() * 2)) / 2
map.value = tempfloat
}
} else if (cmd.sensorType == 5) {
map.value = cmd.scaledSensorValue
map.unit = "%"
map.name = "humidity"
}
sendEvent(name:"temperature", value:map.value)
map
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd)
{
def map = [:]
switch (cmd.operatingState) {
case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE:
map.value = "idle"
break
case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING:
map.value = "heating"
break
}
map.name = "thermostatOperatingState"
if (settings.heatdetails == "No") {
map.displayed = false
}
map
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) {
def map = [:]
switch (cmd.mode) {
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
map.value = "comfort"
break
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF:
map.value = "off"
break
default:
map.value = "eco"
break
}
map.name = "thermostatMode"
sendEvent(name:"thermostatMode", value:map.value)
map
}
def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {
delayBetween([
zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:0).format(),
zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
poll()
], 2300)
}
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
log.debug "Zwave event received: $cmd"
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
log.debug "Zwave event received: $cmd"
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
log.warn "Unexpected zwave command $cmd"
}
// Command Implementations
def poll() {
def weather
// If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast.
if (settings.zipcode) {
log.debug "ZipCode: ${settings.zipcode}"
weather = getWeatherFeature( "conditions", settings.zipcode )
// Check if the variable is populated, otherwise return.
if (!weather) {
log.debug( "Something went wrong, no data found." )
return false
}
// Set the tiles
def locationScale = getTemperatureScale()
def tempToSend
if (locationScale == "C")
{
log.debug( "Outdoor Temperature: ${weather.current_observation.temp_c}ºC" )
sendEvent( name: 'outsideTemp', value: weather.current_observation.temp_c )
tempToSend = weather.current_observation.temp_c
}
else
{
log.debug( "Outdoor Temperature: ${weather.current_observation.temp_f}ºF" )
sendEvent( name: 'outsideTemp', value: weather.current_observation.temp_f )
tempToSend = weather.current_observation.temp_f
}
delayBetween([
quickSetOutTemp(tempToSend),
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format(),
zwave.thermostatModeV2.thermostatModeGet().format(),
zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format(),
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
sendEvent( name: 'change', value: 0 )
], 100)
} else {
delayBetween([
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format(),
zwave.thermostatModeV2.thermostatModeGet().format(),
zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format(),
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
sendEvent( name: 'change', value: 0 )
], 100)
}
}
def refresh() {
poll()
}
def configure() {
poll()
}
def applyNow() {
float currentHeatSetpoint = device.currentValue("heatingSetpoint")
def deviceScale
def locationScale = getTemperatureScale()
sendEvent( name: 'change', value: 0 )
log.debug("currentHeatSetpoint $currentHeatSetpoint")
if (locationScale == "C")
{
deviceScale = 0
}
else
{
deviceScale = 1
}
delayBetween([
zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: state.precision, scaledValue: currentHeatSetpoint).format(),
poll()
], 1000)
}
def quickSetHeat(degrees) {
setHeatingSetpoint(degrees, 0)
}
def setHeatingSetpoint(degrees, delay = 0) {
sendEvent(name:"heatingSetpoint", value:degrees)
applyNow()
}
def quickSetOutTemp(degrees) {
setOutdoorTemperature(degrees, 0)
}
def setOutdoorTemperature(degrees, delay = 0) {
setOutdoorTemperature(degrees.toDouble(), delay)
}
def setOutdoorTemperature(Double degrees, Integer delay = 0) {
def deviceScale
def locationScale = getTemperatureScale()
def p = (state.precision == null) ? 1 : state.precision
if (locationScale == "C")
{
deviceScale = 0
}
else
{
deviceScale = 1
}
log.info "setOutdoorTemperature: ${degrees}"
zwave.sensorMultilevelV3.sensorMultilevelReport(sensorType: 1, scale: deviceScale, precision: p, scaledSensorValue: degrees).format()
}
def increaseHeatSetpoint()
{
def currentMode = device.currentState("thermostatMode")?.value
if (currentMode != "off")
{
float currentSetpoint = device.currentValue("heatingSetpoint")
def locationScale = getTemperatureScale()
float maxSetpoint
float step
if (locationScale == "C")
{
maxSetpoint = 30;
step = 0.5
}
else
{
maxSetpoint = 86
step = 1
}
if (currentSetpoint < maxSetpoint)
{
currentSetpoint = currentSetpoint + step
quickSetHeat(currentSetpoint)
}
}
}
def decreaseHeatSetpoint()
{
def currentMode = device.currentState("thermostatMode")?.value
if (currentMode != "off")
{
float currentSetpoint = device.currentValue("heatingSetpoint")
def locationScale = getTemperatureScale()
float minSetpoint
float step
if (locationScale == "C")
{
minSetpoint = 5;
step = 0.5
}
else
{
minSetpoint = 41
step = 1
}
if (currentSetpoint > minSetpoint)
{
currentSetpoint = currentSetpoint - step
quickSetHeat(currentSetpoint)
}
}
}
def switchMode() {
def currentMode = device.currentState("thermostatMode")?.value
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "comfort"
def supportedModes = getDataByName("supportedModes")
def modeOrder = modes()
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
def nextMode = next(lastTriedMode)
if (supportedModes?.contains(currentMode)) {
while (!supportedModes.contains(nextMode) && nextMode != "comfort") {
nextMode = next(nextMode)
}
}
state.lastTriedMode = nextMode
delayBetween([
zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(),
poll()
], 1000)
}
def modes() {
["comfort", "eco", "off"]
}
def getModeMap() { [
"off": 0,
"comfort": 1,
"eco": 11,
]}
def getDataByName(String name) {
state[name] ?: device.getDataValue(name)
}
def setCoolingSetpoint(coolingSetpoint) {
log.trace "${device.displayName} does not support cool setpoint"
}
def off() {
log.trace "off mode applied"
delayBetween([
zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(),
poll()
], 1000)
}
def heat() {
log.trace "heat mode applied"
delayBetween([
zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(),
poll()
], 1000)
}
def eco() {
log.trace "eco mode applied"
delayBetween([
zwave.thermostatModeV2.thermostatModeSet(mode: 11).format(),
poll()
], 1000)
}
def auto() {
log.trace "${device.displayName} does not support auto mode"
}
def emergencyHeat() {
log.trace "${device.displayName} does not support emergency heat mode"
}
def cool() {
log.trace "${device.displayName} does not support cool mode"
}
def setCustomThermostatMode(mode) {
setThermostatMode(mode)
}
def setThermostatMode(String value) {
delayBetween([
zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(),
poll()
], 1000)
}
def fanOn() {
log.trace "${device.displayName} does not support fan on"
}
def fanAuto() {
log.trace "${device.displayName} does not support fan auto"
}
def fanCirculate() {
log.trace "${device.displayName} does not support fan circulate"
}
def setThermostatFanMode() {
log.trace "${device.displayName} does not support fan mode"
}

View File

@@ -0,0 +1,384 @@
/**
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* BlueIris (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);
}