Compare commits

..

1 Commits

Author SHA1 Message Date
Eric Ryherd
c3d1c70109 MSA-687: I'm resubmitting the EZMultipli for publication as requested.
I originally submitted this back in April.
The device type works so lets get it published for everyone to use!
Thanks,
Eric
2015-11-16 07:55:35 -06:00
37 changed files with 1714 additions and 1432 deletions

View File

@@ -0,0 +1,294 @@
// Express Controls EZMultiPli Multi-sensor
// Motion Sensor - Temperature - Light level - 8 Color Indicator LED - Z-Wave Range Extender - Wall Powered
// driver for SmartThings
// The EZMultiPli is also known as the HSM200 from HomeSeer.com
//
// v0.1.0 - DrZWave - chose better icons, Got color LED to work - first fully functional version
// v0.0.9 - jrs - got the temp and luminance to work. Motion works. Debugging the color wheel.
// v0.0.8 - DrZWave 2/25/2015 - change the color control to be tiles since there are only 8 colors.
// v0.0.7 - jrs - 02/23/2015 - Jim Sulin
metadata {
definition (name: "EZmultiPli", namespace: "DrZWave", author: "Eric Ryherd", oauth: true) {
capability "Motion Sensor"
capability "Temperature Measurement"
capability "Illuminance Measurement"
capability "Switch"
capability "Color Control"
capability "Configuration"
command "setColor"
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x71, 0x31, 0x33, 0x72, 0x86, 0x59, 0x85, 0x70, 0x77, 0x5A, 0x7A, 0x73, 0xEF, 0x20"
} // end definition
simulator {
// messages the device returns in response to commands it receives
status "motion" : "command: 7105000000FF07, payload: 07"
status "no motion" : "command: 7105000000FF07, payload: 00"
for (int i = 0; i <= 100; i += 20) {
status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(
scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage()
}
for (int i = 0; i <= 100; i += 20) {
status "luminance ${i} %": new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(
scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage()
}
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
state "color", action:"setColor"
}
} //end simulator
tiles {
standardTile("motion", "device.motion", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true) {
state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0"
state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
}
standardTile("switch", "device.switch", canChangeIcon: true) {
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
state "turningOn", label:'${name}', icon:"st.switches.light.on", backgroundColor:"#79b821"
state "turningOff", label:'${name}', icon:"st.switches.light.off", backgroundColor:"#ffffff"
}
valueTile("temperature", "device.temperature") {
state "temperature", label:'${currentValue}°', unit:"F", icon:"st.Weather.weather2", // would be better if the units would switch to the desired units of the system (imperial or metric)
backgroundColors:[
[value: 0, color: "#1010ff"], // blue=cold
[value: 65, color: "#a0a0f0"],
[value: 70, color: "#e0e050"],
[value: 75, color: "#f0d030"], // yellow
[value: 80, color: "#fbf020"],
[value: 85, color: "#fbdc01"],
[value: 90, color: "#fb3a01"],
[value: 95, color: "#fb0801"] // red=hot
]
} // icons to use would be st.Weather.weather2 or st.alarm.temperature.normal - see http://scripts.3dgo.net/smartthings/icons/ for a list of icons
valueTile("illuminance", "device.illuminance", inactiveLabel: false) {
// jrs 4/7/2015 - Null on display
// state "luminosity", label:'${currentValue} ${unit}'
state "luminosity", label:'${currentValue}%', unit:"%", icon:"st.illuminance.illuminance.bright",
backgroundColors:[
[value: 25, color: "#404040"],
[value: 50, color: "#808080"],
[value: 75, color: "#a0a0a0"],
[value: 90, color: "#e0e0e0"]
]
}
// jrs 4/7/2015
controlTile("rgbSelector", "device.color", "color", height: 1, width: 1) {
state "color", action:"color control.setColor"
}
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
main ("motion")
details(["motion", "temperature", "illuminance", "configure", "switch", "rgbSelector"])
} // end tiles
preferences {
input "OnTime", "number", title: "No Motion Interval", description: "N minutes lights stay on after no motion detected [0, 1-127]", defaultValue: 10, displayDuringSetup: true, required: false
input "OnLevel", "number", title: "Dimmer Onlevel", description: "Dimmer OnLevel for associated node 2 lights [0, 1-99, -1]", defaultValue: -1, displayDuringSetup: true, required: false
input "LiteMin", "number", title: "Luminance Report Frequency", description: "Luminance report sent every N minutes", defaultValue: 60, displayDuringSetup: true, required: false
input "TempMin", "number", title: "Temperature Report Frequency", description: "Temperature report sent every N minutes", defaultValue: 60, displayDuringSetup: true, required: false
input "TempAdj", "number", title: "Temperature Calibration", description: "Adjust temperature up/down N tenths of a degree F [(-127)-(+128)]", defaultValue: 0, displayDuringSetup: true, required: false
}
} // end metadata
// Parse incoming device messages from device to generate events
def parse(String description){
def result = []
def cmd = zwave.parse(description, [0x31: 5]) // 0x31=SensorMultilevel which we force to be version 5
if (cmd) {
result << createEvent(zwaveEvent(cmd))
}
log.debug "Parse returned ${result}"
return result
}
// Event Generation
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){
def map = [:]
switch (cmd.sensorType) {
case 0x01: // SENSOR_TYPE_TEMPERATURE_VERSION_1
def cmdScale = cmd.scale == 1 ? "F" : "C"
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
map.unit = getTemperatureScale()
map.name = "temperature"
log.debug "Temperature report"
break;
case 0x03 : // SENSOR_TYPE_LUMINANCE_VERSION_1
map.value = cmd.scaledSensorValue.toInteger().toString()
map.unit = "%"
map.name = "illuminance"
log.debug "Luminance report"
break;
}
return map
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
def map = [:]
if (cmd.notificationType==0x07) { // NOTIFICATION_TYPE_BURGLAR
if (cmd.event==0x07 || cmd.event==0x08) {
map.name = "motion"
map.value = "active"
map.descriptionText = "$device.displayName motion detected"
log.debug "motion recognized"
} else if (cmd.event==0) {
map.name = "motion"
map.value = "inactive"
map.descriptionText = "$device.displayName no motion detected"
log.debug "No motion recognized"
}
}
if (map.name != "motion") {
log.debug "unmatched parameters for cmd: ${cmd.toString()}}"
}
return map
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
[name: "switch", value: cmd.value ? "on" : "off", type: "digital"]
}
def on() {
log.debug "Turning Light 'on'"
delayBetween([
zwave.basicV1.basicSet(value: 0xFF).format(),
zwave.basicV1.basicGet().format()
], 500)
}
def off() {
log.debug "Turning Light 'off'"
delayBetween([
zwave.basicV1.basicSet(value: 0x00).format(),
zwave.basicV1.basicGet().format()
], 500)
}
def setColor(value) {
log.debug "setColor() : ${value} and red value is= ${value.red}"
def cmds = []
def myred=value.red >128 ? 255 : 0 // the EZMultiPli has just on/off for each of the 3 channels RGB so convert the 0-255 value into 0 or 255.
def mygreen=value.green >128 ? 255 : 0
def myblue=value.blue>128 ? 255 : 0
// cmds << zwave.colorControlV1.stateSet(stateDataLength: 3, VariantGroup1: [0x02, myred], VariantGroup2:[ 0x03, mygreen], VariantGroup3:[0x04,myblue]).format() // ST support for this command as of 2015/02/23 does not support the color IDs so this command cannot be used.
// So instead we'll use these commands to hack around the lack of support of the above command
cmds << zwave.basicV1.basicSet(value: 0x00).format() // As of 2015/02/23 ST is not supporting stateSet properly but found this hack that works.
if (myred!=0) {
cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x02, startState: myred, ignoreStartState: True, updown: True).format()
cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x02).format()
}
if (mygreen!=0) {
cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x03, startState: mygreen, ignoreStartState: True, updown: True).format()
cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x03).format()
}
if (myblue!=0) {
cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x04, startState: myblue, ignoreStartState: True, updown: True).format()
cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x04).format()
}
delayBetween(cmds, 100)
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
// Handles all Z-Wave commands we aren't interested in
[:]
}
// ensure we are passing acceptable param values for LiteMin & TempMin configs
def checkLiteTempInput(value) {
if (value == null) {
value=60
}
def liteTempVal = value.toInteger()
switch (liteTempVal) {
case { it < 0 }:
return 60 // bad value, set to default
break
case { it > 127 }:
return 127 // bad value, greater then MAX, set to MAX
break
default:
return liteTempVal // acceptable value
}
}
// ensure we are passing acceptable param value for OnTime config
def checkOnTimeInput(value) {
if (value == null) {
value=10
}
def onTimeVal = value.toInteger()
switch (onTimeVal) {
case { it < 0 }:
return 10 // bad value set to default
break
case { it > 127 }:
return 127 // bad value, greater then MAX, set to MAX
break
default:
return onTimeVal // acceptable value
}
}
// ensure we are passing acceptable param value for OnLevel config
def checkOnLevelInput(value) {
if (value == null) {
value=99
}
def onLevelVal = value.toInteger()
switch (onLevelVal) {
case { it < -1 }:
return -1 // bad value set to default
break
case { it > 99 }:
return 99 // bad value, greater then MAX, set to MAX
break
default:
return onLevelVal // acceptable value
}
}
// ensure we are passing an acceptable param value for TempAdj configs
def checkTempAdjInput(value) {
if (value == null) {
value=0
}
def tempAdjVal = value.toInteger()
switch (tempAdjVal) {
case { it < -127 }:
return 0 // bad value, set to default
break
case { it > 128 }:
return 128 // bad value, greater then MAX, set to MAX
break
default:
return tempAdjVal // acceptable value
}
}
def configure() {
log.debug "OnTime=${settings.OnTime} OnLevel=${settings.OnLevel}"
def cmd = delayBetween([
zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, configurationValue: [checkOnTimeInput(settings.OnTime)]).format(),
zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, configurationValue: [checkOnLevelInput(settings.OnLevel)]).format(),
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, configurationValue: [checkLiteTempInput(settings.LiteMin)]).format(),
zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [checkLiteTempInput(settings.TempMin)]).format(),
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, configurationValue: [checkTempAdjInput(settings.TempAdj)]).format()
], 100)
log.debug cmd
cmd
}

View File

@@ -15,7 +15,6 @@
* Author: SmartThings * Author: SmartThings
* Date: 2013-12-04 * Date: 2013-12-04
*/ */
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
metadata { metadata {
definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") { definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") {
capability "Switch Level" capability "Switch Level"
@@ -26,6 +25,7 @@ metadata {
capability "Refresh" capability "Refresh"
capability "Sensor" capability "Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019"
} }
// simulator metadata // simulator metadata

View File

@@ -1,71 +0,0 @@
/**
* Ecobee Sensor
*
* Copyright 2015 Juan Risso
*
* 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.
*
*/
metadata {
definition (name: "Ecobee Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Sensor"
capability "Temperature Measurement"
capability "Motion Sensor"
capability "Refresh"
capability "Polling"
}
simulator {
// TODO: define status and reply messages here
}
tiles {
valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°', unit:"F",
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"]
]
)
}
standardTile("motion", "device.motion") {
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main (["temperature","motion"])
details(["temperature","motion","refresh"])
}
}
def refresh() {
log.debug "refresh..."
poll()
}
void poll() {
log.debug "Executing 'poll' using parent SmartApp"
parent.pollChildren(this)
}
//generate custom mobile activity feeds event
def generateActivityFeedsEvent(notificationMessage) {
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}

View File

@@ -22,31 +22,31 @@ metadata {
capability "Polling" capability "Polling"
capability "Sensor" capability "Sensor"
capability "Refresh" capability "Refresh"
command "generateEvent" command "generateEvent"
command "raiseSetpoint" command "raiseSetpoint"
command "lowerSetpoint" command "lowerSetpoint"
command "resumeProgram" command "resumeProgram"
command "switchMode" command "switchMode"
attribute "thermostatSetpoint","number" attribute "thermostatSetpoint","number"
attribute "thermostatStatus","string" attribute "thermostatStatus","string"
} }
simulator { } simulator { }
tiles { tiles {
valueTile("temperature", "device.temperature", width: 2, height: 2) { valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°', unit:"F", state("temperature", label:'${currentValue}°', unit:"F",
backgroundColors:[ backgroundColors:[
[value: 31, color: "#153591"], [value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"], [value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"], [value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"], [value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"], [value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"], [value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"] [value: 96, color: "#bc2323"]
] ]
) )
} }
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
@@ -54,27 +54,27 @@ metadata {
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat" state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool" state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto" state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat" state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
state "updating", label:"Working", icon: "st.secondary.secondary" state "updating", label:"Working", icon: "st.secondary.secondary"
} }
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on" state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off" state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate" state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto" state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
} }
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up" state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up"
} }
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") { valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
state "thermostatSetpoint", label:'${currentValue}°' state "thermostatSetpoint", label:'${currentValue}'
} }
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") { valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff" state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
} }
standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
state "setpoint", action:"lowerSetpoint", icon:"st.thermostat.thermostat-down" state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down"
} }
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
} }
@@ -91,196 +91,218 @@ metadata {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") { standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") {
state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send" state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon"
state "updating", label:"Working", icon: "st.secondary.secondary"
} }
main "temperature" main "temperature"
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
}
preferences {
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold (default)", required: false, options:["Temporary", "Permanent"]
} }
} }
/*
preferences {
input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80
input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70
input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"]
}
*/
// parse events into attributes // parse events into attributes
def parse(String description) { def parse(String description) {
log.debug "Parsing '${description}'" log.debug "Parsing '${description}'"
// TODO: handle '' attribute // TODO: handle '' attribute
} }
def refresh() { def refresh()
log.debug "refresh called" {
poll() log.debug "refresh called"
log.debug "refresh ended" poll()
log.debug "refresh ended"
} }
def go()
{
log.debug "before:go tile tapped"
poll()
log.debug "after"
}
void poll() { void poll() {
log.debug "Executing 'poll' using parent SmartApp" log.debug "Executing 'poll' using parent SmartApp"
def results = parent.pollChild(this) def results = parent.pollChild(this)
generateEvent(results) //parse received message from parent parseEventData(results)
generateStatusEvent()
} }
def generateEvent(Map results) { def parseEventData(Map results)
{
log.debug "parsing data $results" log.debug "parsing data $results"
if(results) { if(results)
results.each { name, value -> {
results.each { name, value ->
def linkText = getLinkText(device) def linkText = getLinkText(device)
def isChange = false def isChange = false
def isDisplayed = true def isDisplayed = true
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name] if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F
isChange = isTemperatureStateChange(device, name, value.toString()) isChange = isTemperatureStateChange(device, name, value.toString())
isDisplayed = isChange isDisplayed = isChange
event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed]
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ sendEvent(
isChange = isStateChange(device, name, value.toString()) name: name,
event << [value: value.toString(), isStateChange: isChange, displayed: false] value: value,
} else { unit: "F",
isChange = isStateChange(device, name, value.toString()) linkText: linkText,
isDisplayed = isChange descriptionText: getThermostatDescriptionText(name, value, linkText),
event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed] handlerName: name,
} isStateChange: isChange,
sendEvent(event) displayed: isDisplayed)
}
else {
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
sendEvent(
name: name,
value: value.toString(),
linkText: linkText,
descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name,
isStateChange: isChange,
displayed: isDisplayed)
}
}
generateSetpointEvent ()
generateStatusEvent ()
}
}
void generateEvent(Map results)
{
log.debug "parsing data $results"
if(results)
{
results.each { name, value ->
def linkText = getLinkText(device)
def isChange = false
def isDisplayed = true
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
isChange = isTemperatureStateChange(device, name, value.toString())
isDisplayed = isChange
sendEvent(
name: name,
value: value,
unit: "F",
linkText: linkText,
descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name,
isStateChange: isChange,
displayed: isDisplayed)
}
else {
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
sendEvent(
name: name,
value: value.toString(),
linkText: linkText,
descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name,
isStateChange: isChange,
displayed: isDisplayed)
}
} }
generateSetpointEvent () generateSetpointEvent ()
generateStatusEvent () generateStatusEvent()
} }
} }
//return descriptionText to be shown on mobile activity feed private getThermostatDescriptionText(name, value, linkText)
private getThermostatDescriptionText(name, value, linkText) { {
if(name == "temperature") { if(name == "temperature")
return "$linkText temperature is $value°F" {
return "$linkText was $value°F"
} else if(name == "heatingSetpoint") {
return "heating setpoint is $value°F"
} else if(name == "coolingSetpoint"){
return "cooling setpoint is $value°F"
} else if (name == "thermostatMode") {
return "thermostat mode is ${value}"
} else if (name == "thermostatFanMode") {
return "thermostat fan mode is ${value}"
} else {
return "${name} = ${value}"
} }
else if(name == "heatingSetpoint")
{
return "latest heating setpoint was $value°F"
}
else if(name == "coolingSetpoint")
{
return "latest cooling setpoint was $value°F"
}
else if (name == "thermostatMode")
{
return "thermostat mode is ${value}"
}
else
{
return "${name} = ${value}"
}
} }
void setHeatingSetpoint(setpoint) {
setHeatingSetpoint(setpoint.toDouble()) void setHeatingSetpoint(degreesF) {
setHeatingSetpoint(degreesF.toDouble())
} }
void setHeatingSetpoint(Double setpoint) { void setHeatingSetpoint(Double degreesF) {
// def mode = device.currentValue("thermostatMode") log.debug "setHeatingSetpoint({$degreesF})"
def heatingSetpoint = setpoint sendEvent("name":"heatingSetpoint", "value":degreesF)
def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble() Double coolingSetpoint = device.currentValue("coolingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last() log.debug "coolingSetpoint: $coolingSetpoint"
parent.setHold(this, degreesF, coolingSetpoint)
//enforce limits of heatingSetpoint
if (heatingSetpoint > 79) {
heatingSetpoint = 79
} else if (heatingSetpoint < 45) {
heatingSetpoint = 45
}
//enforce limits of heatingSetpoint vs coolingSetpoint
if (heatingSetpoint >= coolingSetpoint) {
coolingSetpoint = heatingSetpoint
}
log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
generateSetpointEvent()
generateStatusEvent()
} else {
log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app
}
} }
void setCoolingSetpoint(setpoint) { void setCoolingSetpoint(degreesF) {
setCoolingSetpoint(setpoint.toDouble()) setCoolingSetpoint(degreesF.toDouble())
} }
void setCoolingSetpoint(Double setpoint) { void setCoolingSetpoint(Double degreesF) {
// def mode = device.currentValue("thermostatMode") log.debug "setCoolingSetpoint({$degreesF})"
def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble() sendEvent("name":"coolingSetpoint", "value":degreesF)
def coolingSetpoint = setpoint Double heatingSetpoint = device.currentValue("heatingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last() parent.setHold(this, heatingSetpoint, degreesF)
if (coolingSetpoint > 92) {
coolingSetpoint = 92
} else if (coolingSetpoint < 65) {
coolingSetpoint = 65
}
//enforce limits of heatingSetpoint vs coolingSetpoint
if (heatingSetpoint >= coolingSetpoint) {
heatingSetpoint = coolingSetpoint
}
log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
generateSetpointEvent()
generateStatusEvent()
} else {
log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app
}
} }
void resumeProgram() { def configure() {
log.debug "resumeProgram() is called" }
sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false)
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.resumeProgram(this, deviceId)) {
sendEvent("name":"thermostatStatus", "value":"setpoint is updating", "description":statusText, displayed: false)
runIn(5, "poll")
log.debug "resumeProgram() is done"
sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
} else {
sendEvent("name":"thermostatStatus", "value":"failed resume click refresh", "description":statusText, displayed: false)
log.error "Error resumeProgram() check parent.resumeProgram(this, deviceId)"
}
def resumeProgram() {
parent.resumeProgram(this)
} }
def modes() { def modes() {
if (state.modes) { if (state.modes) {
log.debug "Modes = ${state.modes}" log.debug "Modes = ${state.modes}"
return state.modes return state.modes
} }
else { else {
state.modes = parent.availableModes(this) state.modes = parent.availableModes(this)
log.debug "Modes = ${state.modes}" log.debug "Modes = ${state.modes}"
return state.modes return state.modes
} }
} }
def fanModes() { def fanModes() {
["off", "on", "auto", "circulate"] ["off", "on", "auto", "circulate"]
} }
def switchMode() { def switchMode() {
log.debug "in switchMode" log.debug "in switchMode"
def currentMode = device.currentState("thermostatMode")?.value def currentMode = device.currentState("thermostatMode")?.value
@@ -292,7 +314,7 @@ def switchMode() {
} }
def switchToMode(nextMode) { def switchToMode(nextMode) {
log.debug "In switchToMode = ${nextMode}" log.debug "In switchToMode = ${nextMode}"
if (nextMode in modes()) { if (nextMode in modes()) {
state.lastTriedMode = nextMode state.lastTriedMode = nextMode
"$nextMode"() "$nextMode"()
@@ -354,326 +376,300 @@ def getDataByName(String name) {
def setThermostatMode(String value) { def setThermostatMode(String value) {
log.debug "setThermostatMode({$value})" log.debug "setThermostatMode({$value})"
} }
def setThermostatFanMode(String value) { def setThermostatFanMode(String value) {
log.debug "setThermostatFanMode({$value})" log.debug "setThermostatFanMode({$value})"
} }
def generateModeEvent(mode) { def generateModeEvent(mode) {
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true)
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true)
} }
def generateFanModeEvent(fanMode) { def generateFanModeEvent(fanMode) {
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true)
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true)
} }
def generateOperatingStateEvent(operatingState) { def generateOperatingStateEvent(operatingState) {
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true)
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true)
} }
def off() { def off() {
log.debug "off" log.debug "off"
def deviceId = device.deviceNetworkId.split(/\./).last() generateModeEvent("off")
if (parent.setMode (this,"off", deviceId)) if (parent.setMode (this,"off"))
generateModeEvent("off") generateModeEvent("off")
else { else {
log.debug "Error setting new mode." log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back generateModeEvent(currentMode) // reset the tile back
} }
generateSetpointEvent() generateSetpointEvent()
generateStatusEvent() generateStatusEvent()
} }
def heat() { def heat() {
log.debug "heat" log.debug "heat"
def deviceId = device.deviceNetworkId.split(/\./).last() generateModeEvent("heat")
if (parent.setMode (this,"heat", deviceId)) if (parent.setMode (this,"heat"))
generateModeEvent("heat") generateModeEvent("heat")
else { else {
log.debug "Error setting new mode." log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back generateModeEvent(currentMode) // reset the tile back
} }
generateSetpointEvent() generateSetpointEvent()
generateStatusEvent() generateStatusEvent()
} }
def auxHeatOnly() { def auxHeatOnly() {
log.debug "auxHeatOnly" log.debug "auxHeatOnly"
def deviceId = device.deviceNetworkId.split(/\./).last() generateModeEvent("auxHeatOnly")
if (parent.setMode (this,"auxHeatOnly", deviceId)) if (parent.setMode (this,"auxHeatOnly"))
generateModeEvent("auxHeatOnly") generateModeEvent("auxHeatOnly")
else { else {
log.debug "Error setting new mode." log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back generateModeEvent(currentMode) // reset the tile back
} }
generateSetpointEvent() generateSetpointEvent()
generateStatusEvent() generateStatusEvent()
} }
def cool() { def cool() {
log.debug "cool" log.debug "cool"
def deviceId = device.deviceNetworkId.split(/\./).last() generateModeEvent("cool")
if (parent.setMode (this,"cool", deviceId)) if (parent.setMode (this,"cool"))
generateModeEvent("cool") generateModeEvent("cool")
else { else {
log.debug "Error setting new mode." log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back generateModeEvent(currentMode) // reset the tile back
} }
generateSetpointEvent() generateSetpointEvent()
generateStatusEvent() generateStatusEvent()
} }
def auto() { def auto() {
log.debug "auto" log.debug "auto"
def deviceId = device.deviceNetworkId.split(/\./).last() generateModeEvent("auto")
if (parent.setMode (this,"auto", deviceId)) if (parent.setMode (this,"auto"))
generateModeEvent("auto") generateModeEvent("auto")
else { else {
log.debug "Error setting new mode." log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value def currentMode = device.currentState("thermostatMode")?.value
generateModeEvent(currentMode) // reset the tile back generateModeEvent(currentMode) // reset the tile back
} }
generateSetpointEvent() generateSetpointEvent()
generateStatusEvent() generateStatusEvent()
} }
def fanOn() { def fanOn() {
log.debug "fanOn" log.debug "fanOn"
// parent.setFanMode (this,"on") parent.setFanMode (this,"on")
} }
def fanAuto() { def fanAuto() {
log.debug "fanAuto" log.debug "fanAuto"
// parent.setFanMode (this,"auto") parent.setFanMode (this,"auto")
} }
def fanCirculate() { def fanCirculate() {
log.debug "fanCirculate" log.debug "fanCirculate"
// parent.setFanMode (this,"circulate") parent.setFanMode (this,"circulate")
} }
def fanOff() { def fanOff() {
log.debug "fanOff" log.debug "fanOff"
// parent.setFanMode (this,"off") parent.setFanMode (this,"off")
} }
def generateSetpointEvent() { def generateSetpointEvent() {
log.debug "Generate SetPoint Event" log.debug "Generate SetPoint Event"
def mode = device.currentValue("thermostatMode") def mode = device.currentValue("thermostatMode")
log.debug "Current Mode = ${mode}" log.debug "Current Mode = ${mode}"
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
log.debug "Heating Setpoint = ${heatingSetpoint}" log.debug "Heating Setpoint = ${heatingSetpoint}"
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
log.debug "Cooling Setpoint = ${coolingSetpoint}" log.debug "Cooling Setpoint = ${coolingSetpoint}"
if (mode == "heat") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
}
else if (mode == "cool") {
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
if (mode == "heat") { } else if (mode == "auto") {
sendEvent("name":"thermostatSetpoint", "value":"Auto")
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) } else if (mode == "off") {
sendEvent("name":"thermostatSetpoint", "value":"Off")
} } else if (mode == "emergencyHeat") {
else if (mode == "cool") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()) }
} else if (mode == "auto") {
sendEvent("name":"thermostatSetpoint", "value":"Auto")
} else if (mode == "off") {
sendEvent("name":"thermostatSetpoint", "value":"Off")
} else if (mode == "emergencyHeat") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
}
} }
void raiseSetpoint() { void raiseSetpoint() {
log.debug "Raise SetPoint"
def mode = device.currentValue("thermostatMode") def mode = device.currentValue("thermostatMode")
def targetvalue def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
log.debug "Current Mode = ${mode}"
if (mode == "heat") {
heatingSetpoint++
if (heatingSetpoint > 99)
heatingSetpoint = 99
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
parent.setHold (this, heatingSetpoint, coolingSetpoint)
log.debug "New Heating Setpoint = ${heatingSetpoint}"
}
else if (mode == "cool") {
coolingSetpoint++
if (coolingSetpoint > 99)
coolingSetpoint = 99
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
parent.setHold (this, heatingSetpoint, coolingSetpoint)
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
}
generateStatusEvent()
if (mode == "off" || mode == "auto") {
log.warn "this mode: $mode does not allow raiseSetpoint"
} else {
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
if (device.latestState('thermostatSetpoint')) {
targetvalue = device.latestState('thermostatSetpoint').value as Integer
} else {
targetvalue = 0
}
targetvalue = targetvalue + 1
if (mode == "heat" && targetvalue > 79) {
targetvalue = 79
} else if (mode == "cool" && targetvalue > 92) {
targetvalue = 92
}
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
log.info "In mode $mode raiseSetpoint() to $targetvalue"
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite
}
} }
//called by tile when user hit raise temperature button on UI
void lowerSetpoint() { void lowerSetpoint() {
log.debug "Lower SetPoint"
def mode = device.currentValue("thermostatMode") def mode = device.currentValue("thermostatMode")
def targetvalue def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}"
if (mode == "heat" || mode == "emergencyHeat") {
heatingSetpoint--
if (heatingSetpoint < 32)
heatingSetpoint = 32
if (mode == "off" || mode == "auto") { sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
log.warn "this mode: $mode does not allow lowerSetpoint" sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
} else {
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() parent.setHold (this, heatingSetpoint, coolingSetpoint)
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger() log.debug "New Heating Setpoint = ${heatingSetpoint}"
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
if (device.latestState('thermostatSetpoint')) { }
targetvalue = device.latestState('thermostatSetpoint').value as Integer else if (mode == "cool") {
} else {
targetvalue = 0 coolingSetpoint--
}
targetvalue = targetvalue - 1 if (coolingSetpoint < 32)
coolingSetpoint = 32
if (mode == "heat" && targetvalue.toInteger() < 45) {
targetvalue = 45 sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
} else if (mode == "cool" && targetvalue.toInteger() < 65) { sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
targetvalue = 65
} parent.setHold (this, heatingSetpoint, coolingSetpoint)
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) log.debug "New Cooling Setpoint = ${coolingSetpoint}"
log.info "In mode $mode lowerSetpoint() to $targetvalue"
}
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite generateStatusEvent()
}
}
//called by raiseSetpoint() and lowerSetpoint()
void alterSetpoint(temp) {
def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def deviceId = device.deviceNetworkId.split(/\./).last()
def targetHeatingSetpoint
def targetCoolingSetpoint
//step1: check thermostatMode, enforce limits before sending request to cloud
if (mode == "heat"){
if (temp.value > coolingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
} else {
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = coolingSetpoint
}
} else if (mode == "cool") {
//enforce limits before sending request to cloud
if (temp.value < heatingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
} else {
targetHeatingSetpoint = heatingSetpoint
targetCoolingSetpoint = temp.value
}
}
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " +
"coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}"
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
//step2: call parent.setHold to send http request to 3rd party cloud
if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) {
sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false)
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
} else {
log.error "Error alterSetpoint()"
if (mode == "heat"){
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
} else if (mode == "cool") {
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
}
}
generateStatusEvent()
} }
def generateStatusEvent() { def generateStatusEvent() {
def mode = device.currentValue("thermostatMode") def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def temperature = device.currentValue("temperature").toInteger() def temperature = device.currentValue("temperature").toInteger()
def statusText def statusText
log.debug "Generate Status Event for Mode = ${mode}" log.debug "Generate Status Event for Mode = ${mode}"
log.debug "Temperature = ${temperature}" log.debug "Temperature = ${temperature}"
log.debug "Heating set point = ${heatingSetpoint}" log.debug "Heating set point = ${heatingSetpoint}"
log.debug "Cooling set point = ${coolingSetpoint}" log.debug "Cooling set point = ${coolingSetpoint}"
log.debug "HVAC Mode = ${mode}" log.debug "HVAC Mode = ${mode}"
if (mode == "heat") { if (mode == "heat") {
if (temperature >= heatingSetpoint) if (temperature >= heatingSetpoint)
statusText = "Right Now: Idle" statusText = "Right Now: Idle"
else else
statusText = "Heating to ${heatingSetpoint}° F" statusText = "Heating to ${heatingSetpoint}° F"
} else if (mode == "cool") { } else if (mode == "cool") {
if (temperature <= coolingSetpoint) if (temperature <= coolingSetpoint)
statusText = "Right Now: Idle" statusText = "Right Now: Idle"
else else
statusText = "Cooling to ${coolingSetpoint}° F" statusText = "Cooling to ${coolingSetpoint}° F"
} else if (mode == "auto") { } else if (mode == "auto") {
statusText = "Right Now: Auto" statusText = "Right Now: Auto"
} else if (mode == "off") { } else if (mode == "off") {
statusText = "Right Now: Off" statusText = "Right Now: Off"
} else if (mode == "emergencyHeat") { } else if (mode == "emergencyHeat") {
statusText = "Emergency Heat" statusText = "Emergency Heat"
} else { } else {
statusText = "?" statusText = "?"
} }
log.debug "Generate Status Event = ${statusText}" log.debug "Generate Status Event = ${statusText}"
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true) sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true)
} }
//generate custom mobile activity feeds event
def generateActivityFeedsEvent(notificationMessage) {
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}

View File

@@ -14,7 +14,7 @@
* *
*/ */
metadata { metadata {
definition (name: "Harmony Activity", namespace: "smartthings", author: "Juan Risso") { definition (name: "Logitech Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
capability "Switch" capability "Switch"
capability "Actuator" capability "Actuator"
capability "Refresh" capability "Refresh"

View File

@@ -17,50 +17,44 @@ metadata {
} }
simulator { simulator {
// TODO: define status and reply messages here
} }
tiles(scale: 2) { tiles {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666" state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
tileAttribute ("device.color", key: "COLOR_CONTROL") {
attributeState "color", action:"setColor"
}
tileAttribute ("device.model", key: "SECONDARY_CONTROL") {
attributeState "model", label: '${currentValue}'
}
} }
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'' state "default", label:''
} }
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") { controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
state "colorTemp", action:"color temperature.setColorTemperature" state "color", action:"setColor"
} }
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) { controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
state "level", label: '${currentValue}%'
}
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") {
state "colorTemp", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
state "colorTemp", label: '${currentValue}K' state "colorTemp", label: '${currentValue}K'
} }
main "switch" main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"])
} }
} }
@@ -76,7 +70,7 @@ def parse(String description) {
def setHue(percentage) { def setHue(percentage) {
log.debug "setHue ${percentage}" log.debug "setHue ${percentage}"
parent.logErrors(logObject: log) { parent.logErrors(logObject: log) {
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "hue:${percentage * 3.6}", power: "on"]) def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "hue", value: percentage) sendEvent(name: "hue", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
@@ -89,7 +83,7 @@ def setHue(percentage) {
def setSaturation(percentage) { def setSaturation(percentage) {
log.debug "setSaturation ${percentage}" log.debug "setSaturation ${percentage}"
parent.logErrors(logObject: log) { parent.logErrors(logObject: log) {
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "saturation:${percentage / 100}", power: "on"]) def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "saturation", value: percentage) sendEvent(name: "saturation", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
@@ -120,7 +114,7 @@ def setColor(Map color) {
} }
} }
parent.logErrors(logObject:log) { parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${selector()}/state", [color: attrs.join(" "), power: "on"]) def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "color", value: color.hex) sendEvent(name: "color", value: color.hex)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
@@ -141,10 +135,9 @@ def setLevel(percentage) {
return off() // if the brightness is set to 0, just turn it off return off() // if the brightness is set to 0, just turn it off
} }
parent.logErrors(logObject:log) { parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${selector()}/state", ["brightness": percentage / 100, "power": "on"]) def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "level", value: percentage) sendEvent(name: "level", value: percentage)
sendEvent(name: "switch.setLevel", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} else { } else {
log.error("Bad setLevel result: [${resp.status}] ${resp.data}") log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
@@ -155,7 +148,7 @@ def setLevel(percentage) {
def setColorTemperature(kelvin) { def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}" log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() { parent.logErrors() {
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"]) def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "colorTemperature", value: kelvin) sendEvent(name: "colorTemperature", value: kelvin)
sendEvent(name: "color", value: "#ffffff") sendEvent(name: "color", value: "#ffffff")
@@ -170,7 +163,7 @@ def setColorTemperature(kelvin) {
def on() { def on() {
log.debug "Device setOn" log.debug "Device setOn"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) { if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} }
} }
@@ -179,7 +172,7 @@ def on() {
def off() { def off() {
log.debug "Device setOff" log.debug "Device setOff"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) { if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
sendEvent(name: "switch", value: "off") sendEvent(name: "switch", value: "off")
} }
} }
@@ -187,26 +180,19 @@ def off() {
def poll() { def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
def resp = parent.apiGET("/lights/${selector()}") def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
if (resp.status == 404) { if (resp.status != 200) {
sendEvent(name: "switch", value: "unreachable")
return []
} else if (resp.status != 200) {
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
return [] return []
} }
def data = resp.data[0] def data = resp.data
log.debug("Data: ${data}")
sendEvent(name: "label", value: data.label) sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100))
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int)) sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
sendEvent(name: "hue", value: data.color.hue / 3.6) sendEvent(name: "hue", value: data.color.hue / 3.6)
sendEvent(name: "saturation", value: data.color.saturation * 100) sendEvent(name: "saturation", value: data.color.saturation * 100)
sendEvent(name: "colorTemperature", value: data.color.kelvin) sendEvent(name: "colorTemperature", value: data.color.kelvin)
sendEvent(name: "model", value: "${data.product.company} ${data.product.name}")
return [] return []
} }
@@ -215,11 +201,3 @@ def refresh() {
log.debug "Executing 'refresh'" log.debug "Executing 'refresh'"
poll() poll()
} }
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -16,44 +16,41 @@ metadata {
} }
simulator { simulator {
// TODO: define status and reply messages here
} }
tiles(scale: 2) { tiles {
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666" state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff" state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState "level", action:"switch level.setLevel"
}
} }
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
} }
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
state "default", label:'' state "default", label:''
} }
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") { controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
state "colorTemp", action:"color temperature.setColorTemperature" state "level", action:"switch level.setLevel"
}
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
state "level", label: '${currentValue}%'
} }
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) { controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
state "colorTemp", action:"color temperature.setColorTemperature"
}
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
state "colorTemp", label: '${currentValue}K' state "colorTemp", label: '${currentValue}K'
} }
main "switch" main(["switch"])
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) details(["switch", "refresh", "level", "levelSliderControl", "colorTempSliderControl", "colorTemp"])
} }
} }
// parse events into attributes // parse events into attributes
@@ -75,10 +72,9 @@ def setLevel(percentage) {
return off() // if the brightness is set to 0, just turn it off return off() // if the brightness is set to 0, just turn it off
} }
parent.logErrors(logObject:log) { parent.logErrors(logObject:log) {
def resp = parent.apiPUT("/lights/${selector()}/state", [brightness: percentage / 100, power: "on"]) def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "level", value: percentage) sendEvent(name: "level", value: percentage)
sendEvent(name: "switch.setLevel", value: percentage)
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} else { } else {
log.error("Bad setLevel result: [${resp.status}] ${resp.data}") log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
@@ -89,7 +85,7 @@ def setLevel(percentage) {
def setColorTemperature(kelvin) { def setColorTemperature(kelvin) {
log.debug "Executing 'setColorTemperature' to ${kelvin}" log.debug "Executing 'setColorTemperature' to ${kelvin}"
parent.logErrors() { parent.logErrors() {
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"]) def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
if (resp.status < 300) { if (resp.status < 300) {
sendEvent(name: "colorTemperature", value: kelvin) sendEvent(name: "colorTemperature", value: kelvin)
sendEvent(name: "color", value: "#ffffff") sendEvent(name: "color", value: "#ffffff")
@@ -104,7 +100,7 @@ def setColorTemperature(kelvin) {
def on() { def on() {
log.debug "Device setOn" log.debug "Device setOn"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) { if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
sendEvent(name: "switch", value: "on") sendEvent(name: "switch", value: "on")
} }
} }
@@ -113,7 +109,7 @@ def on() {
def off() { def off() {
log.debug "Device setOff" log.debug "Device setOff"
parent.logErrors() { parent.logErrors() {
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) { if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
sendEvent(name: "switch", value: "off") sendEvent(name: "switch", value: "off")
} }
} }
@@ -121,22 +117,16 @@ def off() {
def poll() { def poll() {
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
def resp = parent.apiGET("/lights/${selector()}") def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
if (resp.status == 404) { if (resp.status != 200) {
sendEvent(name: "switch", value: "unreachable")
return []
} else if (resp.status != 200) {
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
return [] return []
} }
def data = resp.data[0] def data = resp.data
sendEvent(name: "label", value: data.label) sendEvent(name: "level", value: sprintf("%f", (data.brightness ?: 1) * 100))
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
sendEvent(name: "colorTemperature", value: data.color.kelvin) sendEvent(name: "colorTemperature", value: data.color.kelvin)
sendEvent(name: "model", value: data.product.name)
return [] return []
} }
@@ -145,11 +135,3 @@ def refresh() {
log.debug "Executing 'refresh'" log.debug "Executing 'refresh'"
poll() poll()
} }
def selector() {
if (device.deviceNetworkId.contains(":")) {
return device.deviceNetworkId
} else {
return "id:${device.deviceNetworkId}"
}
}

View File

@@ -79,8 +79,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
} }

View File

@@ -88,8 +88,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
} }

View File

@@ -43,8 +43,8 @@ metadata {
]) ])
} }
section { section {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
} }

View File

@@ -45,8 +45,8 @@ metadata {
]) ])
} }
section { section {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
} }

View File

@@ -37,8 +37,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -61,8 +61,8 @@
]) ])
} }
section { section {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
section { section {
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
@@ -115,30 +115,31 @@
} }
} }
def parse(String description) { def parse(String description) {
Map map = [:]
if (description?.startsWith('catchall:')) { Map map = [:]
map = parseCatchAllMessage(description) if (description?.startsWith('catchall:')) {
} map = parseCatchAllMessage(description)
}
else if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
else if (description?.startsWith('temperature: ')) { else if (description?.startsWith('temperature: ')) {
map = parseCustomMessage(description) map = parseCustomMessage(description)
} }
else if (description?.startsWith('zone status')) { else if (description?.startsWith('zone status')) {
map = parseIasMessage(description) map = parseIasMessage(description)
} }
def result = map ? createEvent(map) : null def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) { if (description?.startsWith('enroll request')) {
List cmds = enrollResponse() List cmds = enrollResponse()
log.debug "enroll response: ${cmds}" log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) } result = cmds?.collect { new physicalgraph.device.HubAction(it) }
} }
else if (description?.startsWith('read attr -')) { return result
result = parseReportAttributeMessage(description).each { createEvent(it) } }
}
return result
}
private Map parseCatchAllMessage(String description) { private Map parseCatchAllMessage(String description) {
Map resultMap = [:] Map resultMap = [:]
@@ -177,40 +178,28 @@ private boolean shouldProcessMessage(cluster) {
return !ignoredMessage return !ignoredMessage
} }
private List parseReportAttributeMessage(String description) { private Map parseReportAttributeMessage(String description) {
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":") def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
} }
List result = [] Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") { if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value) def value = getTemperature(descMap.value)
result << getTemperatureResult(value) resultMap = getTemperatureResult(value)
} }
else if (descMap.cluster == "FC02" && descMap.attrId == "0010") { else if (descMap.cluster == "FC02" && descMap.attrId == "0010") {
if (descMap.value.size() == 32) { resultMap = getAccelerationResult(descMap.value)
// value will look like 00ae29001403e2290013001629001201
// breaking this apart and swapping byte order where appropriate, this breaks down to:
// X (0x0012) = 0x0016
// Y (0x0013) = 0x03E2
// Z (0x0014) = 0x00AE
// note that there is a known bug in that the x,y,z attributes are interpreted in the wrong order
// this will be fixed in a future update
def threeAxisAttributes = descMap.value[0..-9]
result << parseAxis(threeAxisAttributes)
descMap.value = descMap.value[-2..-1]
}
result << getAccelerationResult(descMap.value)
} }
else if (descMap.cluster == "FC02" && descMap.attrId == "0012") { else if (descMap.cluster == "FC02" && descMap.attrId == "0012") {
result << parseAxis(descMap.value) resultMap = parseAxis(descMap.value)
} }
else if (descMap.cluster == "0001" && descMap.attrId == "0020") { else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
result << getBatteryResult(Integer.parseInt(descMap.value, 16)) resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
} }
return result return resultMap
} }
private Map parseCustomMessage(String description) { private Map parseCustomMessage(String description) {

View File

@@ -43,8 +43,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -32,8 +32,8 @@
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -34,8 +34,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -33,8 +33,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -45,8 +45,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
tiles { tiles {

View File

@@ -33,8 +33,8 @@ metadata {
} }
preferences { preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
} }
tiles { tiles {

View File

@@ -33,14 +33,14 @@ metadata {
state "power", label: '${currentValue} W' state "power", label: '${currentValue} W'
} }
htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: "www.wattvision.com" , url: '${currentValue}', width: 3, height: 2) tile(name: "powerChart", attribute: "powerContent", type: "HTML", url: '${currentValue}', width: 3, height: 2) { }
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
} }
main "power" main "power"
details(["powerContent", "power", "refresh"]) details(["powerChart", "power", "refresh"])
} }
} }
@@ -74,10 +74,10 @@ public addWattvisionData(json) {
log.trace "Adding data from Wattvision" log.trace "Adding data from Wattvision"
def data = parseJson(json.data.toString()) def data = json.data
def units = json.units ?: "watts" def units = json.units ?: "watts"
if (data.size() > 0) { if (data) {
def latestData = data[-1] def latestData = data[-1]
data.each { data.each {
sendPowerEvent(it.t, it.v, units, (latestData == it)) sendPowerEvent(it.t, it.v, units, (latestData == it))
@@ -103,7 +103,3 @@ private sendPowerEvent(time, value, units, isLatest = false) {
sendEvent(eventData) sendEvent(eventData)
} }
def parseJson(String s) {
new groovy.json.JsonSlurper().parseText(s)
}

View File

@@ -25,8 +25,8 @@ metadata {
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -17,15 +17,15 @@ metadata {
capability "Actuator" capability "Actuator"
capability "Configuration" capability "Configuration"
capability "Refresh" capability "Refresh"
capability "Sensor"
capability "Switch" capability "Switch"
capability "Switch Level" capability "Switch Level"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
fingerprint profileId: "0104", inClusters: "0001, 0006, 0008, 000E", outClusters: "0019", manufacturer: "Nanoleaf", model: "IvyBulbs", deviceJoinName: "Nanoleaf Smart Bulbs"
} }
tiles(scale: 2) { tiles(scale: 2) {
@@ -87,4 +87,4 @@ def refresh() {
def configure() { def configure() {
log.debug "Configuring Reporting and Bindings." log.debug "Configuring Reporting and Bindings."
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
} }

View File

@@ -23,8 +23,8 @@ metadata {
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "0003, 000A, 0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
} }
tiles(scale: 2) { tiles(scale: 2) {

View File

@@ -17,6 +17,7 @@ metadata {
capability "Actuator" capability "Actuator"
capability "Configuration" capability "Configuration"
capability "Refresh" capability "Refresh"
capability "Sensor"
capability "Switch" capability "Switch"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"

View File

@@ -23,18 +23,18 @@ metadata {
capability "Color Temperature" capability "Color Temperature"
capability "Configuration" capability "Configuration"
capability "Refresh" capability "Refresh"
capability "Sensor"
capability "Switch" capability "Switch"
capability "Switch Level" capability "Switch Level"
attribute "colorName", "string" attribute "colorName", "string"
command "setGenericName" command "setGenericName"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04", outClusters: "0019" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
} }
// UI tile definitions // UI tile definitions

View File

@@ -66,20 +66,9 @@ metadata {
import physicalgraph.zwave.commands.doorlockv1.* import physicalgraph.zwave.commands.doorlockv1.*
import physicalgraph.zwave.commands.usercodev1.* import physicalgraph.zwave.commands.usercodev1.*
def updated() {
try {
if (!state.init) {
state.init = true
response(secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()]))
}
} catch (e) {
log.warn "updated() threw $e"
}
}
def parse(String description) { def parse(String description) {
def result = null def result = null
if (description.startsWith("Err 106")) { if (description.startsWith("Err")) {
if (state.sec) { if (state.sec) {
result = createEvent(descriptionText:description, displayed:false) result = createEvent(descriptionText:description, displayed:false)
} else { } else {
@@ -91,8 +80,6 @@ def parse(String description) {
displayed: true, displayed: true,
) )
} }
} else if (description == "updated") {
return null
} else { } else {
def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ]) def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ])
if (cmd) { if (cmd) {
@@ -299,7 +286,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
} }
break break
case 167: case 167:
if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) { if (!state.lastbatt || (new Date().time) - state.lastbatt > 12*60*60*1000) {
map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ] map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ]
result << response(secure(zwave.batteryV1.batteryGet())) result << response(secure(zwave.batteryV1.batteryGet()))
} else { } else {
@@ -444,7 +431,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
} else { } else {
map.value = cmd.batteryLevel map.value = cmd.batteryLevel
} }
state.lastbatt = now() state.lastbatt = new Date().time
createEvent(map) createEvent(map)
} }
@@ -512,14 +499,15 @@ def refresh() {
cmds << "delay 4200" cmds << "delay 4200"
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
state.associationQuery = now() state.associationQuery = new Date().time
} else if (secondsPast(state.associationQuery, 9)) { } else if (new Date().time - state.associationQuery.toLong() > 9000) {
log.debug "setting association"
cmds << "delay 6000" cmds << "delay 6000"
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
state.associationQuery = now() state.associationQuery = new Date().time
} }
log.debug "refresh sending ${cmds.inspect()}" log.debug "refresh sending ${cmds.inspect()}"
cmds cmds
@@ -527,22 +515,55 @@ def refresh() {
def poll() { def poll() {
def cmds = [] def cmds = []
// Only check lock state if it changed recently or we haven't had an update in an hour if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
def latest = device.currentState("lock")?.date?.time log.debug "setting association"
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) { cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
cmds << secure(zwave.doorLockV1.doorLockOperationGet()) cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
state.lastPoll = now() cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) { cmds << "delay 6000"
cmds << secure(zwave.batteryV1.batteryGet()) cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
state.lastbatt = now() //inside-214 cmds << "delay 6000"
} state.associationQuery = new Date().time
if (cmds) {
log.debug "poll is sending ${cmds.inspect()}"
cmds
} else { } else {
// workaround to keep polling from stopping due to lack of activity // Only check lock state if it changed recently or we haven't had an update in an hour
sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false) def latest = device.currentState("lock")?.date?.time
null if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
state.lastPoll = (new Date()).time
} else if (!state.MSR) {
cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
} else if (!state.fw) {
cmds << zwave.versionV1.versionGet().format()
} else if (!state.codes) {
state.pollCode = 1
cmds << secure(zwave.userCodeV1.usersNumberGet())
} else if (state.pollCode && state.pollCode <= state.codes) {
cmds << requestCode(state.pollCode)
} else if (!state.lastbatt || (new Date().time) - state.lastbatt > 53*60*60*1000) {
cmds << secure(zwave.batteryV1.batteryGet())
} else if (!state.enc) {
encryptCodes()
state.enc = 1
}
}
log.debug "poll is sending ${cmds.inspect()}"
device.activity()
cmds ?: null
}
private def encryptCodes() {
def keys = new ArrayList(state.keySet().findAll { it.startsWith("code") })
keys.each { key ->
def match = (key =~ /^code(\d+)$/)
if (match) try {
def keynum = match[0][1].toInteger()
if (keynum > 30 && !state[key]) {
state.remove(key)
} else if (state[key] && !state[key].startsWith("~")) {
log.debug "encrypting $key: ${state[key].inspect()}"
state[key] = encrypt(state[key])
}
} catch (java.lang.NumberFormatException e) { }
} }
} }
@@ -651,7 +672,7 @@ private Boolean secondsPast(timestamp, seconds) {
return true return true
} }
} }
return (now() - timestamp) > (seconds * 1000) return (new Date().time - timestamp) > (seconds * 1000)
} }
private allCodesDeleted() { private allCodesDeleted() {

View File

@@ -115,10 +115,6 @@ def strobe() {
] ]
} }
def both() {
on()
}
def refresh() { def refresh() {
log.debug "sending battery refresh command" log.debug "sending battery refresh command"
zwave.batteryV1.batteryGet().format() zwave.batteryV1.batteryGet().format()

View File

@@ -246,9 +246,6 @@ def toggle(devices) {
else if (devices*.currentValue('lock').contains('locked')) { else if (devices*.currentValue('lock').contains('locked')) {
devices.unlock() devices.unlock()
} }
else if (devices*.currentValue('lock').contains('unlocked')) {
devices.lock()
}
else if (devices*.currentValue('alarm').contains('off')) { else if (devices*.currentValue('alarm').contains('off')) {
devices.siren() devices.siren()
} }

File diff suppressed because it is too large Load Diff

View File

@@ -592,7 +592,7 @@ def updated() {
// log.debug "External Id=${app.id}:${member.id}" // log.debug "External Id=${app.id}:${member.id}"
// create the device // create the device
def childDevice = addChildDevice("smartthings", "Life360 User", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true]) def childDevice = addChildDevice("smartthings", "life360-user", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
// childDevice.setMemberId(member.id) // childDevice.setMemberId(member.id)
if (childDevice) if (childDevice)

View File

@@ -5,23 +5,23 @@
* *
*/ */
definition( definition(
name: "LIFX (Connect)", name: "LIFX (Connect)",
namespace: "smartthings", namespace: "smartthings",
author: "LIFX", author: "LIFX",
description: "Allows you to use LIFX smart light bulbs with SmartThings.", description: "Allows you to use LIFX smart light bulbs with SmartThings.",
category: "Convenience", category: "Convenience",
iconUrl: "https://cloud.lifx.com/images/lifx.png", iconUrl: "https://cloud.lifx.com/images/lifx.png",
iconX2Url: "https://cloud.lifx.com/images/lifx.png", iconX2Url: "https://cloud.lifx.com/images/lifx.png",
iconX3Url: "https://cloud.lifx.com/images/lifx.png", iconX3Url: "https://cloud.lifx.com/images/lifx.png",
oauth: true, oauth: true,
singleInstance: true) { singleInstance: true) {
appSetting "clientId" appSetting "clientId"
appSetting "clientSecret" appSetting "clientSecret"
} }
preferences { preferences {
page(name: "Credentials", title: "LIFX", content: "authPage", install: true) page(name: "Credentials", title: "LIFX", content: "authPage", install: false)
} }
mappings { mappings {
@@ -33,29 +33,29 @@ mappings {
path("/test") { action: [ GET: "oauthSuccess" ] } path("/test") { action: [ GET: "oauthSuccess" ] }
} }
def getServerUrl() { return "https://graph.api.smartthings.com" } def getServerUrl() { return "https://graph.api.smartthings.com" }
def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback"} def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" }
def apiURL(path = '/') { return "https://api.lifx.com/v1${path}" } def buildRedirectUrl(page) {
def getSecretKey() { return appSettings.secretKey } return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
def getClientId() { return appSettings.clientId } }
def authPage() { def authPage() {
log.debug "authPage test1" log.debug "authPage"
if (!state.lifxAccessToken) { if (!state.lifxAccessToken) {
log.debug "no LIFX access token" log.debug "no LIFX access token"
// This is the SmartThings access token // This is the SmartThings access token
if (!state.accessToken) { if (!state.accessToken) {
log.debug "no access token, create access token" log.debug "no access token, create access token"
state.accessToken = createAccessToken() // predefined method createAccessToken() // predefined method
} }
def description = "Tap to enter LIFX credentials" def description = "Tap to enter LIFX credentials"
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" // this triggers oauthInit() below
// def redirectUrl = "${apiServerUrl}" log.debug "app id: ${app.id}"
log.debug "app id: ${app.id}"
log.debug "redirect url: ${redirectUrl}" log.debug "redirect url: ${redirectUrl}"
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) { return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) {
section { section {
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account") href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
// href(url:buildRedirectUrl("test"), title: "Message test")
} }
} }
} else { } else {
@@ -63,15 +63,17 @@ def authPage() {
def options = locationOptions() ?: [] def options = locationOptions() ?: []
def count = options.size() def count = options.size()
def refreshInterval = 3
return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) { return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
section("Select your location") { section("Select your location") {
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options, submitOnChange: true input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options
} }
} }
} }
} }
// OAuth // OAuth
def oauthInit() { def oauthInit() {
@@ -110,7 +112,7 @@ def oauthCallback() {
} }
def oauthReceiveToken(redirectUrl = null) { def oauthReceiveToken(redirectUrl = null) {
// Not sure what redirectUrl is for
log.debug "receiveToken - params: ${params}" log.debug "receiveToken - params: ${params}"
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here? def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here?
def params = [ def params = [
@@ -133,25 +135,25 @@ def oauthReceiveToken(redirectUrl = null) {
def oauthSuccess() { def oauthSuccess() {
def message = """ def message = """
<p>Your LIFX Account is now connected to SmartThings!</p> <p>Your LIFX Account is now connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p> <p>Click 'Done' to finish setup.</p>
""" """
oauthConnectionStatus(message) oauthConnectionStatus(message)
} }
def oauthFailure() { def oauthFailure() {
def message = """ def message = """
<p>The connection could not be established!</p> <p>The connection could not be established!</p>
<p>Click 'Done' to return to the menu.</p> <p>Click 'Done' to return to the menu.</p>
""" """
oauthConnectionStatus(message) oauthConnectionStatus(message)
} }
def oauthReceivedToken() { def oauthReceivedToken() {
def message = """ def message = """
<p>Your LIFX Account is already connected to SmartThings!</p> <p>Your LIFX Account is already connected to SmartThings!</p>
<p>Click 'Done' to finish setup.</p> <p>Click 'Done' to finish setup.</p>
""" """
oauthConnectionStatus(message) oauthConnectionStatus(message)
} }
@@ -159,74 +161,74 @@ def oauthConnectionStatus(message, redirectUrl = null) {
def redirectHtml = "" def redirectHtml = ""
if (redirectUrl) { if (redirectUrl) {
redirectHtml = """ redirectHtml = """
<meta http-equiv="refresh" content="3; url=${redirectUrl}" /> <meta http-equiv="refresh" content="3; url=${redirectUrl}" />
""" """
} }
def html = """ def html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>SmartThings Connection</title> <title>SmartThings Connection</title>
<style type="text/css"> <style type="text/css">
@font-face { @font-face {
font-family: 'Swiss 721 W01 Thin'; font-family: 'Swiss 721 W01 Thin';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot'); src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'), src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg'); url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'Swiss 721 W01 Light'; font-family: 'Swiss 721 W01 Light';
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot'); src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'), src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'), url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg'); url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
.container { .container {
width: 280; width: 280;
padding: 20px; padding: 20px;
text-align: center; text-align: center;
} }
img { img {
vertical-align: middle; vertical-align: middle;
} }
img:nth-child(2) { img:nth-child(2) {
margin: 0 15px; margin: 0 15px;
} }
p { p {
font-size: 1.2em; font-size: 1.2em;
font-family: 'Swiss 721 W01 Thin'; font-family: 'Swiss 721 W01 Thin';
text-align: center; text-align: center;
color: #666666; color: #666666;
padding: 0 20px; padding: 0 20px;
margin-bottom: 0; margin-bottom: 0;
} }
span { span {
font-family: 'Swiss 721 W01 Light'; font-family: 'Swiss 721 W01 Light';
} }
</style> </style>
${redirectHtml} ${redirectHtml}
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/> <img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/> <img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/> <img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
<p> <p>
${message} ${message}
</p> </p>
</div> </div>
</body> </body>
</html> </html>
""" """
render contentType: 'text/html', data: html render contentType: 'text/html', data: html
} }
@@ -237,6 +239,7 @@ String toQueryString(Map m) {
// App lifecycle hooks // App lifecycle hooks
def installed() { def installed() {
enableCallback() // wtf does this do?
if (!state.accessToken) { if (!state.accessToken) {
createAccessToken() createAccessToken()
} else { } else {
@@ -248,6 +251,7 @@ def installed() {
// called after settings are changed // called after settings are changed
def updated() { def updated() {
enableCallback() // not sure what this does
if (!state.accessToken) { if (!state.accessToken) {
createAccessToken() createAccessToken()
} else { } else {
@@ -301,36 +305,27 @@ def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
state.remove("lifxAccessToken") state.remove("lifxAccessToken")
options.logObject.warn "Access token is not valid" options.logObject.warn "Access token is not valid"
} }
return options.errorReturn return options.errerReturn
} catch (java.net.SocketTimeoutException e) { } catch (java.net.SocketTimeoutException e) {
options.logObject.warn "Connection timed out, not much we can do here" options.logObject.warn "Connection timed out, not much we can do here"
return options.errorReturn return options.errerReturn
} }
} }
def apiGET(path) { def apiGET(path) {
try { httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response -> logResponse(response)
logResponse(response) return response
return response
}
} catch (groovyx.net.http.HttpResponseException e) {
logResponse(e.response)
return e.response
} }
} }
def apiPUT(path, body = [:]) { def apiPUT(path, body = [:]) {
try { log.debug("Beginning API PUT: ${path}, ${body}")
log.debug("Beginning API PUT: ${path}, ${body}") httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response -> logResponse(response)
logResponse(response) return response
return response }
} }
} catch (groovyx.net.http.HttpResponseException e) {
logResponse(e.response)
return e.response
}}
def devicesList(selector = '') { def devicesList(selector = '') {
logErrors([]) { logErrors([]) {
@@ -345,12 +340,12 @@ def devicesList(selector = '') {
} }
Map locationOptions() { Map locationOptions() {
def options = [:] def options = [:]
def devices = devicesList() def devices = devicesList()
devices.each { device -> devices.each { device ->
options[device.location.id] = device.location.name options[device.location.id] = device.location.name
} }
log.debug("Locations: ${options}")
return options return options
} }
@@ -364,32 +359,28 @@ def updateDevices() {
state.devices = [:] state.devices = [:]
} }
def devices = devicesInLocation() def devices = devicesInLocation()
def selectors = [] def deviceIds = devices*.id
log.debug("All selectors: ${selectors}")
devices.each { device -> devices.each { device ->
def childDevice = getChildDevice(device.id) def childDevice = getChildDevice(device.id)
selectors.add("${device.id}")
if (!childDevice) { if (!childDevice) {
log.info("Adding device ${device.id}: ${device.product}") log.info("Adding device ${device.id}: ${device.capabilities}")
def data = [ def data = [
label: device.label, label: device.label,
level: Math.round((device.brightness ?: 1) * 100), level: sprintf("%f", (device.brightness ?: 1) * 100),
switch: device.connected ? device.power : "unreachable", switch: device.connected ? device.power : "unreachable",
colorTemperature: device.color.kelvin colorTemperature: device.color.kelvin
] ]
if (device.product.capabilities.has_color) { if (device.capabilities.has_color) {
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int) data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
data["hue"] = device.color.hue / 3.6 data["hue"] = device.color.hue / 3.6
data["saturation"] = device.color.saturation * 100 data["saturation"] = device.color.saturation * 100
childDevice = addChildDevice(app.namespace, "LIFX Color Bulb", device.id, null, data) childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data)
} else { } else {
childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data) childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
} }
} }
} }
getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each { getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each {
log.info("Deleting ${it.deviceNetworkId}") log.info("Deleting ${it.deviceNetworkId}")
deleteChildDevice(it.deviceNetworkId) deleteChildDevice(it.deviceNetworkId)
} }
@@ -401,4 +392,4 @@ def refreshDevices() {
getChildDevices().each { device -> getChildDevices().each { device ->
device.refresh() device.refresh()
} }
} }

View File

@@ -49,7 +49,6 @@ preferences {
section("Via a push notification and/or an SMS message"){ section("Via a push notification and/or an SMS message"){
input("recipients", "contact", title: "Send notifications to") { input("recipients", "contact", title: "Send notifications to") {
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
paragraph "If outside the US please make sure to enter the proper country code"
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
} }
} }

View File

@@ -10,24 +10,24 @@
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * 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. * for the specific language governing permissions and limitations under the License.
* *
* Speaker Control * Sonos Control
* *
* Author: SmartThings * Author: SmartThings
* *
* Date: 2013-12-10 * Date: 2013-12-10
*/ */
definition( definition(
name: "Speaker Control", name: "Sonos Control",
namespace: "smartthings", namespace: "smartthings",
author: "SmartThings", author: "SmartThings",
description: "Play or pause your Speaker when certain actions take place in your home.", description: "Play or pause your Sonos when certain actions take place in your home.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
) )
preferences { preferences {
page(name: "mainPage", title: "Control your Speaker when something happens", install: true, uninstall: true) page(name: "mainPage", title: "Control your Sonos when something happens", install: true, uninstall: true)
page(name: "timeIntervalInput", title: "Only during a certain time") { page(name: "timeIntervalInput", title: "Only during a certain time") {
section { section {
input "starting", "time", title: "Starting", required: false input "starting", "time", title: "Starting", required: false
@@ -81,7 +81,7 @@ def mainPage() {
] ]
} }
section { section {
input "sonos", "capability.musicPlayer", title: "Speaker music player", required: true input "sonos", "capability.musicPlayer", title: "Sonos music player", required: true
} }
section("More options", hideable: true, hidden: true) { section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false

View File

@@ -10,7 +10,7 @@
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * 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. * for the specific language governing permissions and limitations under the License.
* *
* Speaker Mood Music * Sonos Mood Music
* *
* Author: SmartThings * Author: SmartThings
* Date: 2014-02-12 * Date: 2014-02-12
@@ -65,7 +65,7 @@ private saveSelectedSong() {
} }
definition( definition(
name: "Speaker Mood Music", name: "Sonos Mood Music",
namespace: "smartthings", namespace: "smartthings",
author: "SmartThings", author: "SmartThings",
description: "Plays a selected song or station.", description: "Plays a selected song or station.",
@@ -75,7 +75,7 @@ definition(
) )
preferences { preferences {
page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true) page(name: "mainPage", title: "Play a selected song or station on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true)
page(name: "chooseTrack", title: "Select a song", install: true) page(name: "chooseTrack", title: "Select a song", install: true)
page(name: "timeIntervalInput", title: "Only during a certain time") { page(name: "timeIntervalInput", title: "Only during a certain time") {
section { section {
@@ -125,7 +125,7 @@ def mainPage() {
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
} }
section { section {
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
} }
section("More options", hideable: true, hidden: true) { section("More options", hideable: true, hidden: true) {
input "volume", "number", title: "Set the volume", description: "0-100%", required: false input "volume", "number", title: "Set the volume", description: "0-100%", required: false

View File

@@ -10,23 +10,23 @@
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * 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. * for the specific language governing permissions and limitations under the License.
* *
* Speaker Custom Message * Sonos Custom Message
* *
* Author: SmartThings * Author: SmartThings
* Date: 2014-1-29 * Date: 2014-1-29
*/ */
definition( definition(
name: "Speaker Notify with Sound", name: "Sonos Notify with Sound",
namespace: "smartthings", namespace: "smartthings",
author: "SmartThings", author: "SmartThings",
description: "Play a sound or custom message through your Speaker when the mode changes or other events occur.", description: "Play a sound or custom message through your Sonos when the mode changes or other events occur.",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
) )
preferences { preferences {
page(name: "mainPage", title: "Play a message on your Speaker when something happens", install: true, uninstall: true) page(name: "mainPage", title: "Play a message on your Sonos when something happens", install: true, uninstall: true)
page(name: "chooseTrack", title: "Select a song or station") page(name: "chooseTrack", title: "Select a song or station")
page(name: "timeIntervalInput", title: "Only during a certain time") { page(name: "timeIntervalInput", title: "Only during a certain time") {
section { section {
@@ -92,7 +92,7 @@ def mainPage() {
input "message","text",title:"Play this message", required:false, multiple: false input "message","text",title:"Play this message", required:false, multiple: false
} }
section { section {
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
} }
section("More options", hideable: true, hidden: true) { section("More options", hideable: true, hidden: true) {
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true

View File

@@ -10,23 +10,23 @@
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * 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. * for the specific language governing permissions and limitations under the License.
* *
* Speaker Weather Forecast * Sonos Weather Forecast
* *
* Author: SmartThings * Author: SmartThings
* Date: 2014-1-29 * Date: 2014-1-29
*/ */
definition( definition(
name: "Speaker Weather Forecast", name: "Sonos Weather Forecast",
namespace: "smartthings", namespace: "smartthings",
author: "SmartThings", author: "SmartThings",
description: "Play a weather report through your Speaker when the mode changes or other events occur", description: "Play a weather report through your Sonos when the mode changes or other events occur",
category: "SmartThings Labs", category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
) )
preferences { preferences {
page(name: "mainPage", title: "Play the weather report on your speaker", install: true, uninstall: true) page(name: "mainPage", title: "Play the weather report on your sonos", install: true, uninstall: true)
page(name: "chooseTrack", title: "Select a song or station") page(name: "chooseTrack", title: "Select a song or station")
page(name: "timeIntervalInput", title: "Only during a certain time") { page(name: "timeIntervalInput", title: "Only during a certain time") {
section { section {
@@ -85,7 +85,7 @@ def mainPage() {
) )
} }
section { section {
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
} }
section("More options", hideable: true, hidden: true) { section("More options", hideable: true, hidden: true) {
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true

View File

@@ -78,7 +78,7 @@ def firstPage()
def motionsDiscovered = motionsDiscovered() def motionsDiscovered = motionsDiscovered()
def lightSwitchesDiscovered = lightSwitchesDiscovered() def lightSwitchesDiscovered = lightSwitchesDiscovered()
return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: selectedSwitches != null || selectedMotions != null || selectedLightSwitches != null) {
section("Select a device...") { section("Select a device...") {
input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered
input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered