Compare commits

..

1 Commits

Author SHA1 Message Date
Vladimir
06a95dda00 MSA-1386: nest proper 2016-07-05 18:23:34 -05:00
17 changed files with 581 additions and 242 deletions

View File

@@ -0,0 +1,471 @@
/**
* Nest Protect (Direct)
* Author: chad@monroe.io
* Author: nick@nickhbailey.com
* Author: dianoga7@3dgo.net
* Date: 2016.01.24
*
*
* INSTALLATION
* =========================================
* 1) Create a new device type from code (https://graph.api.smartthings.com/ide/devices)
* Copy and paste the below, save, publish "For Me"
*
* 2) Create a new device (https://graph.api.smartthings.com/device/list)
* Name: Your Choice
* Device Network Id: Your Choice
* Type: Nest Protect (should be the last option)
* Location: Choose the correct location
* Hub/Group: Leave blank
*
* 3) Update device preferences
* Click on the new device to see the details.
* Click the edit button next to Preferences
* Fill in your information.
* To find your serial number, login to http://home.nest.com. Click on the smoke detector
* you want to see. Under settings, go to Technical Info. Your serial number is
* the second item.
*
* Original design/inspiration provided by:
* -> https://github.com/sidjohn1/SmartThings-NestProtect
* -> https://gist.github.com/Dianoga/6055918
*
* Copyright (C) 2016 Chad Monroe <chad@monroe.io>
* Copyright (C) 2014 Nick Bailey <nick@nickhbailey.com>
* Copyright (C) 2013 Brian Steere <dianoga7@3dgo.net>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions: The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**/
/**
* Static info
*/
private NEST_LOGIN_URL() { "https://home.nest.com/user/login" }
private USER_AGENT_STR() { "Nest/1.1.0.10 CFNetwork/548.0.4" }
preferences
{
input( "username", "text", title: "Username", description: "Your Nest Username (usually an email address)", required: true, displayDuringSetup: true )
input( "password", "password", title: "Password", description: "Your Nest Password", required: true, displayDuringSetup: true )
input( "mac", "text", title: "MAC Address", description: "The MAC address of your smoke detector", required: true, displayDuringSetup: true )
}
metadata
{
definition( name: "Nest Protect - Direct", author: "chad@monroe.io", namespace: "cmonroe" )
{
capability "Polling"
capability "Refresh"
capability "Battery"
capability "Smoke Detector"
capability "Carbon Monoxide Detector"
attribute "alarm_state", "string"
attribute "night_light", "string"
attribute "line_power", "string"
attribute "co_previous_peak", "string"
attribute "wifi_ip", "string"
attribute "version_hw", "string"
attribute "version_sw", "string"
attribute "secondary_status", "string"
}
simulator
{
/* TODO */
}
tiles( scale: 2 )
{
multiAttributeTile( name:"alarm_state", type: "lighting", width: 6, height: 4 )
{
tileAttribute( "device.alarm_state", key: "PRIMARY_CONTROL" )
{
attributeState( "default", label:'--', icon: "st.unknown.unknown.unknown" )
attributeState( "clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#44b621" )
attributeState( "smoke", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13" )
attributeState( "co", label:"CO", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13" )
attributeState( "tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13" )
}
tileAttribute( "device.status_text", key: "SECONDARY_CONTROL" )
{
attributeState( "status_text", label: '${currentValue}', unit:"" )
}
}
standardTile( "smoke", "device.smoke", width: 2, height: 2 )
{
state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
state( "clear", label:"OK", icon:"st.alarm.smoke.clear", backgroundColor:"#44B621" )
state( "detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13" )
state( "tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13" )
}
standardTile( "carbonMonoxide", "device.carbonMonoxide", width: 2, height: 2 )
{
state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
state( "clear", label:"OK", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#44B621" )
state( "detected", label:"CO", icon:"st.alarm.carbon-monoxide.clear", backgroundColor:"#e86d13" )
state( "tested", label:"TEST", icon:"st.alarm.carbon-monoxide.test", backgroundColor:"#e86d13" )
}
standardTile( "night_light", "device.night_light", width: 2, height: 2 )
{
state( "default", label:'UNK', icon: "st.unknown.unknown.unknown" )
state( "unk", label:'UNK', icon: "st.unknown.unknown.unknown" )
state( "on", label: 'ON', icon: "st.switches.light.on", backgroundColor: "#44B621" )
state( "low", label: 'LOW', icon: "st.switches.light.on", backgroundColor: "#44B621" )
state( "med", label: 'MED', icon: "st.switches.light.on", backgroundColor: "#44B621" )
state( "high", label: 'HIGH', icon: "st.switches.light.on", backgroundColor: "#44B621" )
state( "off", label: 'OFF', icon: "st.switches.light.off", backgroundColor: "#ffffff" )
}
valueTile( "version_hw", "device.version_hw", width: 2, height: 2, decoration: "flat" )
{
state( "default", label: 'Hardware ${currentValue}' )
}
valueTile( "co_previous_peak", "device.co_previous_peak", width: 2, height: 2 )
{
state( "co_previous_peak", label: '${currentValue}', unit: "ppm",
backgroundColors: [
[value: 69, color: "#44B621"],
[value: 70, color: "#e86d13"]
]
)
}
valueTile( "version_sw", "device.version_sw", width: 2, height: 2, decoration: "flat" )
{
state( "default", label: 'Software ${currentValue}' )
}
standardTile("refresh", "device.refresh", inactiveLabel: false, width: 2, height: 2, decoration: "flat")
{
state( "default", label:'refresh', action:"polling.poll", icon:"st.secondary.refresh-icon" )
}
valueTile("wifi_ip", "device.wifi_ip", inactiveLabel: false, width: 4, height: 2, decoration: "flat")
{
state( "default", label:'IP: ${currentValue}', height: 1, width: 2, inactiveLabel: false )
}
main "alarm_state"
details( ["alarm_state", "smoke", "carbonMonoxide", "night_light", "version_hw", "co_previous_peak", "version_sw", "wifi_ip", "refresh"] )
}
}
/**
* handle commands
*/
def installed()
{
log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Installed"
do_update()
}
def initialize()
{
log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Initialized"
do_update()
}
def updated()
{
log.info "Nest Protect - Direct ${textVersion()}: ${textCopyright()} Updated"
data.auth = null
}
def poll()
{
log.debug "poll for protect with MAC: " + settings.mac.toUpperCase()
do_update()
}
def refresh()
{
log.debug "refresh for protect with MAC: " + settings.mac.toUpperCase()
do_update()
}
def reschedule()
{
log.debug "re-scheduling update for protect with MAC: " + settings.mac.toUpperCase()
runIn( 300, 'do_update' )
}
def do_update()
{
log.debug "refresh for device with MAC: " + settings.mac.toUpperCase()
api_exec( 'status', [] )
{
def status_text = ""
data.topaz = it.data.topaz.getAt( settings.mac.toUpperCase() )
//log.debug data.topaz
data.topaz.smoke_status = data.topaz.smoke_status == 0 ? 'clear' : 'detected'
data.topaz.co_status = data.topaz.co_status == 0 ? 'clear' : 'detected'
data.topaz.battery_health_state = data.topaz.battery_health_state == 0 ? 'ok' : 'low'
data.topaz.kl_software_version = "v" + data.topaz.kl_software_version.split('Software ')[-1]
data.topaz.model = "v" + data.topaz.model.split('-')[-1]
if ( data.topaz.night_light_enable )
{
switch ( data.topaz.night_light_brightness )
{
case 1:
data.topaz.night_light_brightness = "low"
break
case 2:
data.topaz.night_light_brightness = "med"
break
case 3:
data.topaz.night_light_brightness = "high"
break
default:
data.topaz.night_light_brightness = "on"
break
}
}
else
{
data.topaz.night_light_brightness = "off"
}
if ( data.topaz.line_power_present )
{
data.topaz.line_power_present = "ok"
}
else
{
data.topaz.line_power_present = "dead"
}
if ( !data.topaz.co_previous_peak )
{
/* protect 2.0 units do not support this */
data.topaz.co_previous_peak = 'N/A'
}
else
{
data.topaz.co_previous_peak = "${data.topaz.co_previous_peak}ppm"
}
sendEvent( name: 'smoke', value: data.topaz.smoke_status, descriptionText: "${device.displayName} smoke ${data.topaz.smoke_status}", displayed: false )
sendEvent( name: 'carbonMonoxide', value: data.topaz.co_status, descriptionText: "${device.displayName} carbon monoxide ${data.topaz.co_status}", displayed: false )
sendEvent( name: 'battery', value: data.topaz.battery_health_state, descriptionText: "${device.displayName} battery is ${data.topaz.battery_health_state}", displayed: false )
sendEvent( name: 'night_light', value: data.topaz.night_light_brightness, descriptionText: "${device.displayName} night light is ${data.topaz.night_light_brightness}", displayed: true )
sendEvent( name: 'line_power', value: data.topaz.line_power_present, descriptionText: "${device.displayName} line power is ${data.topaz.line_power_present}", displayed: false )
sendEvent( name: 'co_previous_peak', value: data.topaz.co_previous_peak, descriptionText: "${device.displayName} previous CO peak (PPM) is ${data.topaz.co_previous_peak}", displayed: false )
sendEvent( name: 'wifi_ip', value: data.topaz.wifi_ip_address, descriptionText: "${device.displayName} WiFi IP is ${data.topaz.wifi_ip_address}", displayed: false )
sendEvent( name: 'version_hw', value: data.topaz.model, descriptionText: "${device.displayName} hardware model is ${data.topaz.model}", displayed: false )
sendEvent( name: 'version_sw', value: data.topaz.kl_software_version, descriptionText: "${device.displayName} software version is ${data.topaz.kl_software_version}", displayed: false )
app_alarm_sm()
status_text = "Line Power: ${device.currentState('line_power').value} Battery: ${device.currentState('battery').value}"
sendEvent( name: 'status_text', value: status_text, descriptionText: status_text, displayed: false )
log.debug "Smoke: ${data.topaz.smoke_status}"
log.debug "CO: ${data.topaz.co_status}"
log.debug "Battery: ${data.topaz.battery_health_state}"
log.debug "Night Light: ${data.topaz.night_light_brightness}"
log.debug "Line Power: ${data.topaz.line_power_present}"
log.debug "CO Previous Peak (PPM): ${data.topaz.co_previous_peak}"
log.debug "WiFi IP: ${data.topaz.wifi_ip_address}"
log.debug "Hardware Version: ${data.topaz.model}"
log.debug "Software Version: ${data.topaz.kl_software_version}"
}
reschedule()
}
/**
* state machine for setting global alarm state of app
*/
def app_alarm_sm()
{
def alarm_state = "clear"
def smoke = data.topaz.smoke_status
def co = data.topaz.co_status
switch( smoke )
{
case 'clear':
if ( co != "clear" )
{
alarm_state = "co"
}
break
case 'detected':
alarm_state = "smoke"
break
case 'tested':
default:
/**
* ensure that real co alarm is not set before sending tested alarm for smoke
*/
if ( co == 'detected' )
{
alarm_state = "co"
}
break
}
log.info "alarm state machine finished, sending event.."
log.info "alarm_state: ${alarm_state} smoke: ${smoke} CO: ${co}"
sendEvent( name: 'alarm_state', value: alarm_state, descriptionText: "Alarm: ${alarm_state} (Smoke/CO: ${smoke}/${co})", type: "physical", displayed: true, isStateChange: true )
}
/**
* main entry point for nest API calls
*/
def api_exec(method, args = [], success = {})
{
log.debug "API exec method: ${method} with args: ${args}"
if( !logged_in() )
{
log.debug "login required"
login(method, args, success)
return
}
if( method == null )
{
log.info "API exec with no method passed and we are already logged in; bailing"
return
}
def methods =
[
'status':
[
uri: "/v2/mobile/${data.auth.user}", type: 'get'
],
]
def request = methods.getAt( method )
log.debug "already logged in"
handle_request( request.uri, args, request.type, success )
}
/**
* handle_request() only works once logged in, therefor
* call api_exec() rather than this method directly.
*/
def handle_request(uri, args, type, success)
{
log.debug "handling request type: ${type} at URI: ${uri} with args: ${args}"
if( uri.charAt(0) == '/' )
{
uri = "${data.auth.urls.transport_url}${uri}"
}
def params =
[
uri: uri,
headers:
[
'X-nl-protocol-version': 1,
'X-nl-user-id': data.auth.userid,
'Authorization': "Basic ${data.auth.access_token}",
'Accept-Language': 'en-us',
'userAgent': USER_AGENT_STR()
],
body: args
]
def post_request = { response ->
if( response.getStatus() == 302 )
{
def locations = response.getHeaders( "Location" )
def location = locations[0].getValue()
log.debug "redirecting to ${location}"
handle_request( location, args, type, success )
}
else
{
success.call( response )
}
}
try
{
if( type == 'get' )
{
httpGet( params, post_request )
}
}
catch( Throwable e )
{
login()
}
}
def login(method = null, args = [], success = {})
{
def params =
[
uri: NEST_LOGIN_URL(),
body: [ username: settings.username, password: settings.password ]
]
httpPost( params ) { response ->
data.auth = response.data
data.auth.expires_in = Date.parse('EEE, dd-MMM-yyyy HH:mm:ss z', response.data.expires_in).getTime()
log.debug data.auth
api_exec( method, args, success )
}
}
def logged_in()
{
if( !data.auth )
{
log.debug "data.auth is missing, not logged in"
return false
}
def now = new Date().getTime();
return( data.auth.expires_in > now )
}
private def textVersion()
{
def text = "Version 1.6"
}
private def textCopyright()
{
def text = "Copyright © 2016 Chad Monroe <chad@monroe.io>"
}

View File

@@ -22,7 +22,6 @@ metadata {
capability "Configuration" capability "Configuration"
capability "Sensor" capability "Sensor"
capability "Battery" capability "Battery"
capability "Health Check"
attribute "tamper", "enum", ["detected", "clear"] attribute "tamper", "enum", ["detected", "clear"]
attribute "batteryStatus", "string" attribute "batteryStatus", "string"
@@ -327,9 +326,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
} }
def configure() { def configure() {
// allow device user configured or default 16 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2* timeOptionValueMap[reportInterval] ?: 2*8*60, displayed: false)
// This sensor joins as a secure device if you double-click the button to include it // This sensor joins as a secure device if you double-click the button to include it
log.debug "${device.displayName} is configuring its settings" log.debug "${device.displayName} is configuring its settings"
def request = [] def request = []

View File

@@ -20,9 +20,6 @@ metadata {
capability "Configuration" capability "Configuration"
capability "Sensor" capability "Sensor"
capability "Battery" capability "Battery"
capability "Health Check"
command "configureAfterSecure"
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A" fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A"
} }
@@ -248,8 +245,6 @@ def configureAfterSecure() {
def configure() { def configure() {
// log.debug "configure()" // log.debug "configure()"
//["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet()) //["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet())
// allow device 16 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2*8*60, displayed: false)
} }
private setConfigured() { private setConfigured() {

View File

@@ -20,7 +20,6 @@ metadata {
capability "Illuminance Measurement" capability "Illuminance Measurement"
capability "Sensor" capability "Sensor"
capability "Battery" capability "Battery"
capability "Health Check"
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86" fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
} }
@@ -181,9 +180,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
} }
def configure() { def configure() {
// allow device 10 min to check in; double the periodic reporting interval
sendEvent(name: "checkInterval", value: 2*5*60, displayed: false)
delayBetween([ delayBetween([
// send binary sensor report instead of basic set for motion // send binary sensor report instead of basic set for motion
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(), zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(),

View File

@@ -255,8 +255,7 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
} }
} }

View File

@@ -271,8 +271,7 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
} }
} }

View File

@@ -240,8 +240,7 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }
} }

View File

@@ -338,8 +338,7 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
} }
} }

View File

@@ -234,8 +234,7 @@ def getTemperature(value) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }

View File

@@ -229,8 +229,7 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }

View File

@@ -205,8 +205,7 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }

View File

@@ -223,8 +223,7 @@ private Map getBatteryResult(rawValue) {
def minVolts = 2.1 def minVolts = 2.1
def maxVolts = 3.0 def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts) def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100) result.value = Math.min(100, (int) pct * 100)
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery was ${result.value}%" result.descriptionText = "${linkText} battery was ${result.value}%"
} }

View File

@@ -28,6 +28,7 @@ metadata {
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98" fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98"
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82" fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82"
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters:"0x20" // Philio multi+ fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters:"0x20" // Philio multi+
fingerprint deviceId: "0x0701", inClusters: "0x5E,0x72,0x5A,0x80,0x73,0x84,0x85,0x59,0x71,0x70,0x7A,0x98" // Vision door/window
} }
// simulator metadata // simulator metadata
@@ -82,12 +83,12 @@ def updated() {
cmds = [ cmds = [
command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()), command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()),
"delay 1200", "delay 1200",
zwave.wakeUpV1.wakeUpNoMoreInformation().format() zwave.wakeUpV1.wakeUpNoMoreInformation()
] ]
} else if (!state.lastbat) { } else if (!state.lastbat) {
cmds = [] cmds = []
} else { } else {
cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation()]
} }
response(cmds) response(cmds)
} }

View File

@@ -1,131 +0,0 @@
/**
* LifX Scene Manager
*
* Copyright 2016 Patrick Killian
*
* 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: "LifX Scene Manager",
namespace: "climbingcoder",
author: "Patrick Killian",
description: "When your mode changes, automatically activate a Lifx lighting scene with the same name",
category: "Convenience",
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")
preferences {
section("Configuration") {
input(name:"apikey", type:"text", title:"LifX API Token", required:true)
input(name:"fadeTime", type:"number", title:"Fade Time", description:"Seconds of fade between scenes", required:true)
}
section("Getting a LifX API Token") {
paragraph "1. Go to https://cloud.lifx.com/sign_in and sign in with your LifX account"
paragraph "2. Click on your email address and then choose settings from the drop down menu"
paragraph "3. Click 'Generate Token' and give your token any name"
paragraph "4. Copy your token to the required field above and save it somewhere else for reference"
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
subscribe(location, "mode", modeChangeHandler)
}
def modeChangeHandler(evt) {
log.debug "mode changed to ${evt.value}"
def sceneUUID = ""
sceneUUID = findMatchingLifxScene("${evt.value}")
if (sceneUUID == "") {
log.debug "No matching scene found"
} else {
activateLifxScene("${sceneUUID}")
}
}
/**
* Returns the UUID of a LifX scene that matches
* the passed in sceneName by obtaining a list
* of all available scenes and comparing them
* to the passed in name
*
* @param sceneName - the name of the desired scene
*
* @return UUID - the UUID of any matching scenes found
*/
def findMatchingLifxScene(sceneName) {
log.debug ("Getting list of LifX scenes")
def params = [
headers: ["Authorization": "Bearer ${settings.apikey}"],
uri: "https://api.lifx.com/v1/scenes",
body: []
]
def uuid = ""
try {
httpGet(params) { resp ->
log.debug "response status code: ${resp.status}"
resp.data.each {
log.debug("Found scene '${it.name}'")
if ("${it.name}" == sceneName) {
uuid = "${it.uuid}"
}
}
}
} catch (e) {
log.error "something went wrong: $e"
} finally {
return uuid
}
}
/**
* Calls the Lifx http endpoint to activate a scene with
* the given UUID
*
* @param UUID - the UUID of the scene to activate
*/
def activateLifxScene(UUID) {
log.debug "Activating scene with UUID '${UUID}'"
def params = [
headers: ["Authorization": "Bearer ${settings.apikey}"],
uri: "https://api.lifx.com/v1/scenes/scene_id:${UUID}/activate",
body: [
duration: "${settings.fadeTime}"
]
]
try {
httpPut(params) { resp ->
log.debug "response status code: ${resp.status}"
}
} catch (e) {
log.error "something went wrong: $e"
}
}

View File

@@ -720,7 +720,7 @@ def completionPercentage() {
def now = new Date().getTime() def now = new Date().getTime()
def timeElapsed = now - atomicState.start def timeElapsed = now - atomicState.start
def totalRunTime = totalRunTimeMillis() ?: 1 def totalRunTime = totalRunTimeMillis()
def percentComplete = timeElapsed / totalRunTime * 100 def percentComplete = timeElapsed / totalRunTime * 100
log.debug "percentComplete: ${percentComplete}" log.debug "percentComplete: ${percentComplete}"

View File

@@ -689,7 +689,7 @@ def parse(childDevice, description) {
log.warn "Parsing Body failed - trying again..." log.warn "Parsing Body failed - trying again..."
poll() poll()
} }
if (body instanceof java.util.Map) { if (body instanceof java.util.HashMap) {
//poll response //poll response
def bulbs = getChildDevices() def bulbs = getChildDevices()
for (bulb in body) { for (bulb in body) {

View File

@@ -688,7 +688,7 @@ def validateCommand(device, command) {
def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
def currentDeviceCapability = getCapabilityName(device) def currentDeviceCapability = getCapabilityName(device)
if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) { if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) {
return (command in capabilityCommands[currentDeviceCapability] || (currentDeviceCapability == "Switch" && command == "setLevel" && device.hasCommand("setLevel"))) ? true : false return command in capabilityCommands[currentDeviceCapability] ? true : false
} else { } else {
// Handling other device types here, which don't accept commands // Handling other device types here, which don't accept commands
httpError(400, "Bad request.") httpError(400, "Bad request.")
@@ -823,8 +823,8 @@ def deviceHandler(evt) {
} }
def sendToHarmony(evt, String callbackUrl) { def sendToHarmony(evt, String callbackUrl) {
def callback = new URI(callbackUrl) def callback = new URI(callbackUrl)
if (callback.port != -1) { if(isIP(callback.host)){
def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
sendHubCommand(new physicalgraph.device.HubAction( sendHubCommand(new physicalgraph.device.HubAction(
@@ -852,6 +852,25 @@ def sendToHarmony(evt, String callbackUrl) {
} }
} }
public static boolean isIP(String str) {
try {
String[] parts = str.split("\\.");
if (parts.length != 4) return false;
for (int i = 0; i < 4; ++i) {
int p
try {
p = Integer.parseInt(parts[i]);
} catch (Exception e) {
return false;
}
if (p > 255 || p < 0) return false;
}
return true;
} catch (Exception e) {
return false;
}
}
def listHubs() { def listHubs() {
location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.collect { hubItem(it) } location.hubs?.findAll { it.type.toString() == "PHYSICAL" }?.collect { hubItem(it) }
} }