Compare commits

...

30 Commits

Author SHA1 Message Date
Alexis Lutun
7650e0ebed MSA-895: Hello,
I'm working for NodOn, we are a z-wave plus devices design company : http://www.nodon.fr/en/

I'm a developper in NodOn and actually working on the integration of our products into the smarthings hub.
I'm submitting my first device type handler (more will follow soon),

The device that i'm submitting is this one : http://nodon.fr/en/z-wave/octan-remote_7-2

The Octan Remote is a magnetic remote and can be fixed on any king of metallic area but it is also delivered with its wall mounting plate which can be easily installed on a wall.

The device is attend to be use with the button capability with the pushed and held action, so 8 differents actions total.

Also in the device type handler i add four tiles that give the user the possiblites to send the same button event as if they use the remote. 

If there is anything i forgot or did wrong please let me know i will be more than happy to answer your request

Best Regars
2016-02-21 23:50:08 -06:00
Yaima
85175eb298 Merge pull request #529 from Yaima/master
Ecobee - Changed tiles order
2016-02-19 14:45:01 -08:00
Luke Bredeson
c75568bcf1 Merge pull request #518 from lbredeso/wemo-subscription-improvements
INSIDE-787: Improve Wemo (Connect) event subscriptions
2016-02-19 10:10:23 -06:00
Tom Manley
edc98e4840 Merge pull request #497 from tpmanley/feature/outlet_fingerprint
outlet: Added fingerprint for new ST outlet
2016-02-18 19:25:39 -06:00
Vinay Rao
fb2c2cb2a7 Merge pull request #530 from workingmonk/iris_motion_contact
[DEVC-259] Iris motion and contact sensor fingerprints
2016-02-18 17:22:56 -08:00
Vinay Rao
62aeb0533d Iris motion and contact sensor fingeprints 2016-02-18 17:07:11 -08:00
Yaima Valdivia
fb6cbcc35e Merge branch 'master' of github.com:SmartThingsCommunity/SmartThingsPublic
# Via Yaima
* 'master' of github.com:SmartThingsCommunity/SmartThingsPublic:
2016-02-18 15:50:54 -08:00
Yaima Valdivia
097584944e Ecobee - Changed tiles order 2016-02-18 15:48:30 -08:00
Yaima
01fae3dcd4 Merge pull request #525 from Yaima/master
Better exception handling for Ecobee
2016-02-18 15:13:34 -08:00
Yaima Valdivia
6c125fe80f Better exception handling of Ecobee
Refreshing only if status code 14 - Authentication token has expired.
Refresh your tokens.
2016-02-18 14:49:26 -08:00
Yaima
5728f08770 Merge pull request #522 from Yaima/master
Fixed Ecobee - HH errors
2016-02-17 13:28:40 -08:00
Yaima Valdivia
f073df0a57 Fixed Ecobee - HH errors 2016-02-17 13:28:10 -08:00
Yaima
2af0db4e89 Merge pull request #520 from Yaima/master
Ecobee fanMode available - https://smartthings.atlassian.net/browse/D…
2016-02-17 11:30:43 -08:00
Yaima Valdivia
24bfb7f20f Ecobee fanMode available - https://smartthings.atlassian.net/browse/DVCSMP-1501
https://smartthings.atlassian.net/browse/DVCSMP-1501
2016-02-17 11:15:26 -08:00
Luke Bredeson
9263107f0e INSIDE-787: Improve Wemo (Connect) event subscriptions 2016-02-16 17:05:09 -06:00
Yaima
41b9d71e3d Merge pull request #516 from Yaima/master
Ecobee 3 - https://smartthings.atlassian.net/browse/DEVC-285
2016-02-16 14:52:55 -08:00
Duncan McKee
e60a9d1925 Merge pull request #487 from dantheman2865/MSA-866-2
MSA-866: Update SmartSense Moisture to include Temperature Measurement from WWA02AA
2016-02-16 17:48:29 -05:00
Yaima Valdivia
27b7c24536 Merge branch 'master' of github.com:SmartThingsCommunity/SmartThingsPublic
# By Juan Pablo Risso (3) and others
# Via Kris Schaller (24) and others
* 'master' of github.com:SmartThingsCommunity/SmartThingsPublic:
  PROB-870 - Harmony fails to save credentials
  add missing translations
  add event translation
  Removed canInstallLabs()
  remove segmented style input to prevent iOS crash
  PROB-537 - Fix error in line 335
  MSA-68: Spruce Irrigation controller and soil moisture sensors.
  # This is a combination of 3 commits. # The first commit's message is: MSA-68: Spruce Irrigation controller and soil moisture sensors.
  DVCSMP-1480 Fixed ArrayIndexOutOfBoundsException
  Convert closure to method
  Revert "Convert closure to method"
  Closure was causing sandbox issues locally
  Bugfixes for codeReports
  Fix Homeseer Multi Instance encap parse PROB-398
  Merge pull request #135 from kwarodom/fibaroSmokeSensor
2016-02-16 14:12:07 -08:00
Yaima Valdivia
13d9137c9a Ecobee 3 - https://smartthings.atlassian.net/browse/DEVC-285
https://smartthings.atlassian.net/browse/DEVC-285
https://smartthings.atlassian.net/browse/DVCSMP-1431
2016-02-16 13:48:47 -08:00
Juan Pablo Risso
b672a0b810 Merge pull request #514 from juano2310/logitech_hotfix
PROB-870 - Harmony fails to save credentials
2016-02-16 15:47:26 -05:00
Dylan Bijnagte
2b6a6a47ce Merge pull request #375 from Bijnagte/notify-me-when-i18n-events
add event translation
2016-02-16 14:39:29 -06:00
Juan Pablo Risso
7d07b93694 PROB-870 - Harmony fails to save credentials
It seams like the user removed some activities on Harmony side without removing them from SmartThings. This is causing an issue when adding new activities. This fix checks if the activity still exists before creating a new device.
2016-02-16 14:38:37 -05:00
dylanbijnagte
512bd3adc4 add missing translations 2016-02-16 11:30:20 -06:00
Steve Vlaminck
7f707a9dbb Merge pull request #512 from vlaminck/gentleWakeUp-iOS-crash-fix
remove segmented style input to prevent iOS crash
2016-02-16 10:24:47 -06:00
dylanbijnagte
86e097ba0a add event translation 2016-02-16 10:05:46 -06:00
Juan Pablo Risso
89ec1f207f Merge pull request #513 from juano2310/hue_bu
Removed canInstallLabs()
2016-02-16 10:07:45 -05:00
Juan Pablo Risso
664af57708 Removed canInstallLabs() 2016-02-16 09:35:21 -05:00
vlaminck
d9aa1e378d remove segmented style input to prevent iOS crash 2016-02-15 21:39:06 -06:00
Tom Manley
6c5b93da87 outlet: Added fingerprint for new ST outlet
Resolves:
    https://smartthings.atlassian.net/browse/DVCSMP-1360
2016-02-10 10:43:21 -06:00
Daniel Kurin
b7484ff0b8 MSA-866: Currently, the SmartSense Moisture handles both the FortrezZ WWA01 and the '02, however the '02 sends a temperature measurement. This PR expands the existing DH to add a valueTile with that temperature data (per https://github.com/SmartThingsCommunity/SmartThingsPublic/pull/447) 2016-02-07 11:36:24 -05:00
14 changed files with 884 additions and 455 deletions

View File

@@ -0,0 +1,201 @@
/**
* Copyright 2015 NodOn
*
* 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: "Nodon Octan Remote", namespace: "NodOn", author: "Alexis Lutun") {
capability "Actuator"
capability "Button"
capability "Configuration"
capability "Sleep Sensor"
capability "Battery"
command "pushButtonOne"
command "pushButtonTwo"
command "pushButtonThree"
command "pushButtonFour"
command "buttonEvent"
command "buttonPushed"
command "buttonPushed", [int]
command "refresh"
fingerprint deviceId: "0x0101", inClusters: "0x5E,0x85,0x59,0x80,0x5B,0x70,0x5A,0x72,0x73,0x86,0x84,0xEF,0x5E,0x5B,0x2B,0x27,0x22,0x20,0x26,0x84"
}
tiles(scale: 2) {
standardTile("Octan", "device.button", width: 1, height: 1)
{
state "default", label: "", icon:"http://nodon.fr/smarthings/octan-remote/octanfullicon.png", backgroundColor: "#ffffff"
}
multiAttributeTile(name:"BatteryTile", type: "generic", width: 6, height: 4)
{
tileAttribute ("device.battery", key: "PRIMARY_CONTROL")
{
attributeState "default", icon:"http://nodon.fr/smarthings/octan-remote/octanfullicon.png" , backgroundColor: "#f58220", decoration: "flat"
}
tileAttribute ("device.battery", key: "SECONDARY_CONTROL")
{
attributeState "default", label:'${currentValue}% battery', unit:"%"
}
}
standardTile("button One", "device.button", width: 2, height: 2, decoration: "flat")
{
state "default", label: "", action: "pushButtonOne", icon:"http://nodon.fr/smarthings/octan-remote/1line.png",defaultState: true, backgroundColor: "#ffffff"
state "pushed", label: "", action: "pushButtonOne", icon:"http://nodon.fr/smarthings/octan-remote/1fill.png", backgroundColor: "#ffffff"
}
standardTile("button Two", "device.button", width: 2, height: 2, decoration: "flat")
{
state "default", label: "",action: "pushButtonTwo", icon:"http://nodon.fr/smarthings/octan-remote/2line.png", defaultState: true, backgroundColor: "#ffffff"
state "pushed", label: "",action: "pushButtonTwo", icon:"http://nodon.fr/smarthings/octan-remote/2fill.png", backgroundColor: "#ffffff"
}
standardTile("button Three", "device.button", width: 2, height: 2, decoration: "flat")
{
state "default", label: "", action: "pushButtonThree", icon: "http://nodon.fr/smarthings/octan-remote/3line.png", defaultState: true, backgroundColor: "#ffffff"
state "pushed", label: "", action: "pushButtonThree", icon: "http://nodon.fr/smarthings/octan-remote/3fill.png", backgroundColor: "#ffffff"
}
standardTile("button Four", "device.button", width: 2, height: 2,decoration: "flat")
{
state "default", label: "",action: "pushButtonFour", icon:"http://nodon.fr/smarthings/octan-remote/4line.png", defaultState: true, backgroundColor: "#ffffff"
state "pushed", label: "",action: "pushButtonFour", icon:"http://nodon.fr/smarthings/octan-remote/4fill.png", backgroundColor: "#ffffff"
}
standardTile("refresh", "generic", inactiveLabel: false, decoration: "flat", width: 2, height: 2)
{
state "default", label:'', action: "refresh", icon:"st.secondary.refresh"
}
standardTile("configure", "device.Configuration", decoration: "flat", width: 2, height: 2)
{
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}
main "Octan"
details(["BatteryTile", "button One", "button Two", "configure", "button Three", "button Four", "refresh"])
}
}
def parse(String description)
{
def results = []
if (description.startsWith("Err"))
{
results = createEvent(descriptionText:description, displayed:true)
}
else
{
def cmd = zwave.parse(description, [0x5B: 1, 0x80: 1, 0x84: 1]) //Central Scene , battery, wake up
if(cmd) results += zwaveEvent(cmd)
if(!results) results = [ descriptionText: cmd, displayed: false ]
}
//log.debug("Parsed '$description' to $results")
return results
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
{
def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: false)]
def prevBattery = device.currentState("battery")
if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) //
{
results << response(zwave.batteryV1.batteryGet().format())
createEvent(name: "battery", value: "10", descriptionText: "battery is now ${currentValue}%", isStateChange: true, displayed: true)
}
results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
//log.debug("Wake up")
return results
}
def buttonEvent(button, attribute)
{
if (attribute)
{
createEvent(name: "button", value: "pushed", data: [buttonNumber: button, action: "held"] ,descriptionText: "$device.displayName button $button was held", icon:"http://nodon.fr/smarthings/octan-remote/octandiskfill.png", isStateChange: true, displayed: true)
}
else
{
createEvent(name: "button", value: "pushed", data: [buttonNumber: button, action: "pushed"] ,descriptionText: "$device.displayName button $button was pressed", icon:"http://nodon.fr/smarthings/octan-remote/octandiskfill.png", isStateChange: true, displayed: true)
}
}
def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd)
{
Integer sceneNumber = cmd.sceneNumber as Integer
Integer keyAttributes = cmd.keyAttributes as Integer
Integer sequenceNumber = cmd.sequenceNumber as Integer
buttonEvent(sceneNumber, keyAttributes)
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd)
{
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF)
{
map.value = 1
map.descriptionText = "${device.displayName} has a low battery"
}
else
{
map.value = cmd.batteryLevel
}
createEvent(map)
}
def zwaveEvent(physicalgraph.zwave.Command cmd)
{
[ descriptionText: "$device.displayName: $cmd", linkText:device.displayName, displayed: false ]
}
def configurationCmds()
{
[
zwave.configurationV1.configurationSet(parameterNumber: 8, scaledConfigurationValue:2).format(), //Configure LED to feedback user when command of button has been acknowloedge by the static controller
zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId).format() // Set hub id in lifeline of product so product know the destination of the central scene notification command
]
}
def refresh()
{
def cmd = zwave.batteryV1.batteryGet().format()
return (response(cmd))
}
def configure()
{
def cmd = configurationCmds()
log.debug("Sending configuration: $cmd")
return response(cmd)
}
def pushButtonOne()
{
buttonPushed(1)
}
def pushButtonTwo()
{
buttonPushed(2)
}
def buttonPushed(button)
{
sendEvent(name: "button", value: "pushed", data: [buttonNumber: button, action: "pushed"], descriptionText: "$device.displayName button $button was pushed", icon:"http://nodon.fr/smarthings/octan-remote/octanfullicon.png", isStateChange: true)
}
def pushButtonThree()
{
buttonPushed(3)
}
def pushButtonFour()
{
buttonPushed(4)
}

View File

@@ -22,10 +22,6 @@ metadata {
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",
@@ -56,16 +52,12 @@ metadata {
}
def refresh() {
log.debug "refresh..."
log.debug "refresh called"
poll()
}
void poll() {
log.debug "Executing 'poll' using parent SmartApp"
parent.pollChildren(this)
}
parent.pollChild(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

@@ -19,34 +19,40 @@ metadata {
definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") {
capability "Actuator"
capability "Thermostat"
capability "Temperature Measurement"
capability "Polling"
capability "Sensor"
capability "Refresh"
capability "Refresh"
capability "Relative Humidity Measurement"
command "generateEvent"
command "raiseSetpoint"
command "lowerSetpoint"
command "resumeProgram"
command "switchMode"
command "generateEvent"
command "raiseSetpoint"
command "lowerSetpoint"
command "resumeProgram"
command "switchMode"
command "switchFanMode"
attribute "thermostatSetpoint","number"
attribute "thermostatStatus","string"
attribute "thermostatSetpoint","number"
attribute "thermostatStatus","string"
attribute "maxHeatingSetpoint", "number"
attribute "minHeatingSetpoint", "number"
attribute "maxCoolingSetpoint", "number"
attribute "minCoolingSetpoint", "number"
attribute "deviceTemperatureUnit", "number"
}
simulator { }
tiles {
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"]
]
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("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
@@ -58,10 +64,9 @@ metadata {
state "updating", label:"Working", icon: "st.secondary.secondary"
}
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
state "auto", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-auto"
state "on", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-on"
state "updating", label:"Working", icon: "st.secondary.secondary"
}
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
@@ -91,11 +96,14 @@ metadata {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
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", action:"resumeProgram", nextState: "updating", label:'Resume', icon:"st.samsung.da.oven_ic_send"
state "updating", label:"Working", icon: "st.secondary.secondary"
}
valueTile("humidity", "device.humidity", decoration: "flat") {
state "humidity", label:'${currentValue}%'
}
main "temperature"
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "fanMode","humidity", "resumeProgram", "refresh"])
}
preferences {
@@ -107,8 +115,6 @@ metadata {
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
// TODO: handle '' attribute
}
def refresh() {
@@ -133,16 +139,27 @@ def generateEvent(Map results) {
def isChange = false
def isDisplayed = true
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name]
handlerName: name]
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
isChange = isTemperatureStateChange(device, name, value.toString())
isDisplayed = isChange
event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed]
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
event << [value: sendValue, displayed: false]
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false]
} else if (name=="thermostatFanMode"){
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false]
} else if (name=="humidity") {
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"]
} else {
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
@@ -158,13 +175,19 @@ def generateEvent(Map results) {
//return descriptionText to be shown on mobile activity feed
private getThermostatDescriptionText(name, value, linkText) {
if(name == "temperature") {
return "$linkText temperature is $value°F"
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
return "$linkText temperature is $sendValue ${location.temperatureScale}"
} else if(name == "heatingSetpoint") {
return "heating setpoint is $value°F"
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
return "heating setpoint is $sendValue ${location.temperatureScale}"
} else if(name == "coolingSetpoint"){
return "cooling setpoint is $value°F"
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
return "cooling setpoint is $sendValue ${location.temperatureScale}"
} else if (name == "thermostatMode") {
return "thermostat mode is ${value}"
@@ -172,26 +195,26 @@ private getThermostatDescriptionText(name, value, linkText) {
} else if (name == "thermostatFanMode") {
return "thermostat fan mode is ${value}"
} else if (name == "humidity") {
return "humidity is ${value} %"
} else {
return "${name} = ${value}"
}
}
void setHeatingSetpoint(setpoint) {
setHeatingSetpoint(setpoint.toDouble())
}
void setHeatingSetpoint(Double setpoint) {
// def mode = device.currentValue("thermostatMode")
log.debug "***heating setpoint $setpoint"
def heatingSetpoint = setpoint
def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble()
def coolingSetpoint = device.currentValue("coolingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last()
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
//enforce limits of heatingSetpoint
if (heatingSetpoint > 79) {
heatingSetpoint = 79
} else if (heatingSetpoint < 45) {
heatingSetpoint = 45
if (heatingSetpoint > maxHeatingSetpoint) {
heatingSetpoint = maxHeatingSetpoint
} else if (heatingSetpoint < minHeatingSetpoint) {
heatingSetpoint = minHeatingSetpoint
}
//enforce limits of heatingSetpoint vs coolingSetpoint
@@ -201,32 +224,34 @@ void setHeatingSetpoint(Double setpoint) {
log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
if (parent.setHold(this, heatingValue, coolingValue, 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
log.error "Error setHeatingSetpoint(setpoint)"
}
}
void setCoolingSetpoint(setpoint) {
setCoolingSetpoint(setpoint.toDouble())
}
void setCoolingSetpoint(Double setpoint) {
// def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble()
log.debug "***cooling setpoint $setpoint"
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = setpoint
def deviceId = device.deviceNetworkId.split(/\./).last()
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
if (coolingSetpoint > 92) {
coolingSetpoint = 92
} else if (coolingSetpoint < 65) {
coolingSetpoint = 65
if (coolingSetpoint > maxCoolingSetpoint) {
coolingSetpoint = maxCoolingSetpoint
} else if (coolingSetpoint < minCoolingSetpoint) {
coolingSetpoint = minCoolingSetpoint
}
//enforce limits of heatingSetpoint vs coolingSetpoint
@@ -236,15 +261,18 @@ void setCoolingSetpoint(Double setpoint) {
log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
if (parent.setHold(this, heatingValue, coolingValue, 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
log.error "Error setCoolingSetpoint(setpoint)"
}
}
@@ -278,7 +306,7 @@ def modes() {
}
def fanModes() {
["off", "on", "auto", "circulate"]
["on", "auto"]
}
def switchMode() {
@@ -307,17 +335,15 @@ def switchFanMode() {
def returnCommand
switch (currentFanMode) {
case "fanAuto":
returnCommand = switchToFanMode("fanOn")
case "on":
returnCommand = switchToFanMode("auto")
break
case "fanOn":
returnCommand = switchToFanMode("fanCirculate")
break
case "fanCirculate":
returnCommand = switchToFanMode("fanAuto")
case "auto":
returnCommand = switchToFanMode("on")
break
}
if(!currentFanMode) { returnCommand = switchToFanMode("fanOn") }
if(!currentFanMode) { returnCommand = switchToFanMode("auto") }
returnCommand
}
@@ -326,25 +352,20 @@ def switchToFanMode(nextMode) {
log.debug "switching to fan mode: $nextMode"
def returnCommand
if(nextMode == "fanAuto") {
if(!fanModes.contains("fanAuto")) {
if(nextMode == "auto") {
if(!fanModes.contains("auto")) {
returnCommand = fanAuto()
} else {
returnCommand = switchToFanMode("fanOn")
returnCommand = switchToFanMode("on")
}
} else if(nextMode == "fanOn") {
if(!fanModes.contains("fanOn")) {
} else if(nextMode == "on") {
if(!fanModes.contains("on")) {
returnCommand = fanOn()
} else {
returnCommand = switchToFanMode("fanCirculate")
}
} else if(nextMode == "fanCirculate") {
if(!fanModes.contains("fanCirculate")) {
returnCommand = fanCirculate()
} else {
returnCommand = switchToFanMode("fanAuto")
returnCommand = switchToFanMode("auto")
}
}
returnCommand
}
@@ -354,13 +375,10 @@ def getDataByName(String name) {
def setThermostatMode(String value) {
log.debug "setThermostatMode({$value})"
}
def setThermostatFanMode(String value) {
log.debug "setThermostatFanMode({$value})"
}
def generateModeEvent(mode) {
@@ -447,26 +465,44 @@ def auto() {
def fanOn() {
log.debug "fanOn"
// parent.setFanMode (this,"on")
String fanMode = "on"
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last()
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
generateFanModeEvent(fanMode)
} else {
log.debug "Error setting new mode."
def currentFanMode = device.currentState("thermostatFanMode")?.value
generateModeEvent(currentFanMode) // reset the tile back
}
}
def fanAuto() {
log.debug "fanAuto"
// parent.setFanMode (this,"auto")
String fanMode = "auto"
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last()
}
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
def fanCirculate() {
log.debug "fanCirculate"
// parent.setFanMode (this,"circulate")
}
def fanOff() {
log.debug "fanOff"
// parent.setFanMode (this,"off")
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
generateFanModeEvent(fanMode)
} else {
log.debug "Error setting new mode."
def currentFanMode = device.currentState("thermostatFanMode")?.value
generateModeEvent(currentFanMode) // reset the tile back
}
}
def generateSetpointEvent() {
@@ -476,20 +512,42 @@ def generateSetpointEvent() {
def mode = device.currentValue("thermostatMode")
log.debug "Current Mode = ${mode}"
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def heatingSetpoint = device.currentValue("heatingSetpoint")
log.debug "Heating Setpoint = ${heatingSetpoint}"
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint")
log.debug "Cooling Setpoint = ${coolingSetpoint}"
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
if(location.temperatureScale == "C")
{
maxHeatingSetpoint = roundC(maxHeatingSetpoint)
maxCoolingSetpoint = roundC(maxCoolingSetpoint)
minHeatingSetpoint = roundC(minHeatingSetpoint)
minCoolingSetpoint = roundC(minCoolingSetpoint)
heatingSetpoint = roundC(heatingSetpoint)
coolingSetpoint = roundC(coolingSetpoint)
}
sendEvent("name":"maxHeatingSetpoint", "value":maxHeatingSetpoint, "unit":location.temperatureScale)
sendEvent("name":"maxCoolingSetpoint", "value":maxCoolingSetpoint, "unit":location.temperatureScale)
sendEvent("name":"minHeatingSetpoint", "value":minHeatingSetpoint, "unit":location.temperatureScale)
sendEvent("name":"minCoolingSetpoint", "value":minCoolingSetpoint, "unit":location.temperatureScale)
if (mode == "heat") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint )
}
else if (mode == "cool") {
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString())
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint)
} else if (mode == "auto") {
@@ -499,9 +557,9 @@ def generateSetpointEvent() {
sendEvent("name":"thermostatSetpoint", "value":"Off")
} else if (mode == "emergencyHeat") {
} else if (mode == "auxHeatOnly") {
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint)
}
@@ -510,26 +568,30 @@ def generateSetpointEvent() {
void raiseSetpoint() {
def mode = device.currentValue("thermostatMode")
def targetvalue
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
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()
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def thermostatSetpoint = device.currentValue("thermostatSetpoint")
log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
if (device.latestState('thermostatSetpoint')) {
targetvalue = device.latestState('thermostatSetpoint').value as Integer
targetvalue = device.latestState('thermostatSetpoint').value
targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble()
} else {
targetvalue = 0
}
targetvalue = targetvalue + 1
targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5
if (mode == "heat" && targetvalue > 79) {
targetvalue = 79
} else if (mode == "cool" && targetvalue > 92) {
targetvalue = 92
if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue > maxHeatingSetpoint) {
targetvalue = maxHeatingSetpoint
} else if (mode == "cool" && targetvalue > maxCoolingSetpoint) {
targetvalue = maxCoolingSetpoint
}
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
@@ -543,25 +605,29 @@ void raiseSetpoint() {
void lowerSetpoint() {
def mode = device.currentValue("thermostatMode")
def targetvalue
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
if (mode == "off" || mode == "auto") {
log.warn "this mode: $mode does not allow lowerSetpoint"
} else {
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def thermostatSetpoint = device.currentValue("thermostatSetpoint")
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
if (device.latestState('thermostatSetpoint')) {
targetvalue = device.latestState('thermostatSetpoint').value as Integer
targetvalue = device.latestState('thermostatSetpoint').value
targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble()
} else {
targetvalue = 0
}
targetvalue = targetvalue - 1
targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5
if (mode == "heat" && targetvalue.toInteger() < 45) {
targetvalue = 45
} else if (mode == "cool" && targetvalue.toInteger() < 65) {
targetvalue = 65
if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue < minHeatingSetpoint) {
targetvalue = minHeatingSetpoint
} else if (mode == "cool" && targetvalue < minCoolingSetpoint) {
targetvalue = minCoolingSetpoint
}
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
@@ -575,15 +641,15 @@ void lowerSetpoint() {
void alterSetpoint(temp) {
def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def deviceId = device.deviceNetworkId.split(/\./).last()
def targetHeatingSetpoint
def targetCoolingSetpoint
//step1: check thermostatMode, enforce limits before sending request to cloud
if (mode == "heat"){
if (mode == "heat" || mode == "auxHeatOnly"){
if (temp.value > coolingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
@@ -602,19 +668,22 @@ void alterSetpoint(temp) {
}
}
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " +
"coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}"
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)
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
sendEvent("name": "thermostatSetpoint", "value": temp.value, 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"){
if (mode == "heat" || mode == "auxHeatOnly"){
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
} else if (mode == "cool") {
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
@@ -626,9 +695,9 @@ void alterSetpoint(temp) {
def generateStatusEvent() {
def mode = device.currentValue("thermostatMode")
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
def temperature = device.currentValue("temperature").toInteger()
def heatingSetpoint = device.currentValue("heatingSetpoint")
def coolingSetpoint = device.currentValue("coolingSetpoint")
def temperature = device.currentValue("temperature")
def statusText
@@ -643,14 +712,14 @@ def generateStatusEvent() {
if (temperature >= heatingSetpoint)
statusText = "Right Now: Idle"
else
statusText = "Heating to ${heatingSetpoint}° F"
statusText = "Heating to ${heatingSetpoint} ${location.temperatureScale}"
} else if (mode == "cool") {
if (temperature <= coolingSetpoint)
statusText = "Right Now: Idle"
else
statusText = "Cooling to ${coolingSetpoint}° F"
statusText = "Cooling to ${coolingSetpoint} ${location.temperatureScale}"
} else if (mode == "auto") {
@@ -660,7 +729,7 @@ def generateStatusEvent() {
statusText = "Right Now: Off"
} else if (mode == "emergencyHeat") {
} else if (mode == "auxHeatOnly") {
statusText = "Emergency Heat"
@@ -673,7 +742,18 @@ def generateStatusEvent() {
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true)
}
//generate custom mobile activity feeds event
def generateActivityFeedsEvent(notificationMessage) {
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}
def roundC (tempC) {
return (Math.round(tempC.toDouble() * 2))/2
}
def convertFtoC (tempF) {
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
}
def convertCtoF (tempC) {
return (Math.round(tempC * (9/5)) + 32).toInteger()
}

View File

@@ -16,6 +16,7 @@ metadata {
capability "Water Sensor"
capability "Sensor"
capability "Battery"
capability "Temperature Measurement"
fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86"
fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86"
@@ -39,17 +40,29 @@ metadata {
attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0"
}
}
standardTile("temperature", "device.temperature", width: 2, height: 2) {
standardTile("temperatureState", "device.temperature", width: 2, height: 2) {
state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff"
state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0"
state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000"
}
valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°',
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label:'${currentValue}% battery', unit:""
}
main (["water", "temperature"])
details(["water", "temperature", "battery"])
main (["water", "temperatureState"])
details(["water", "temperatureState", "temperature", "battery"])
}
}
@@ -115,7 +128,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd)
map.descriptionText = "${device.displayName} is ${map.value}"
}
if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) {
map.name = "temperature"
map.name = "temperatureState"
if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"}
if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"}
if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"}
@@ -129,17 +142,30 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd)
map
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd)
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
{
def map = [:]
map.name = "water"
map.value = cmd.value ? "wet" : "dry"
map.descriptionText = "${device.displayName} is ${map.value}"
if(cmd.sensorType == 1) {
map.name = "temperature"
if(cmd.scale == 0) {
map.value = getTemperature(cmd.scaledSensorValue)
} else {
map.value = cmd.scaledSensorValue
}
map.unit = location.temperatureScale
}
map
}
def getTemperature(value) {
if(location.temperatureScale == "C"){
return value
} else {
return Math.round(celsiusToFahrenheit(value))
}
}
def zwaveEvent(physicalgraph.zwave.Command cmd)
{
log.debug "COMMAND CLASS: $cmd"
}
}

View File

@@ -29,6 +29,7 @@ metadata {
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326-L", deviceJoinName: "Iris Motion Sensor"
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
}

View File

@@ -16,17 +16,18 @@
metadata {
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
capability "Battery"
capability "Battery"
capability "Configuration"
capability "Contact Sensor"
capability "Contact Sensor"
capability "Refresh"
capability "Temperature Measurement"
command "enrollResponse"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300"
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3320-L", deviceJoinName: "Iris Contact Sensor"
}
simulator {

View File

@@ -25,6 +25,7 @@ metadata {
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, 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, 000F, 0B04", outClusters: "0019", manufacturer: "SmartThings", model: "outletv4", deviceJoinName: "Outlet"
}
tiles(scale: 2) {

View File

@@ -28,7 +28,7 @@ definition(
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png",
singleInstance: true
singleInstance: true
) {
appSetting "clientId"
}
@@ -61,7 +61,7 @@ def authPage() {
description = "Click to enter Ecobee Credentials"
}
def redirectUrl = buildRedirectUrl //"${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}"
def redirectUrl = buildRedirectUrl
log.debug "RedirectUrl = ${redirectUrl}"
// get rid of next button until the user is actually auth'd
if (!oauthTokenProvided) {
@@ -103,7 +103,7 @@ def oauthInitUrl() {
scope: "smartRead,smartWrite",
client_id: smartThingsClientId,
state: atomicState.oauthInitState,
redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback"
redirect_uri: callbackUrl
]
redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}")
@@ -115,14 +115,13 @@ def callback() {
def code = params.code
def oauthState = params.state
//verify oauthState == atomicState.oauthInitState, so the callback corresponds to the authentication request
if (oauthState == atomicState.oauthInitState){
def tokenParams = [
grant_type: "authorization_code",
code : code,
client_id : smartThingsClientId,
redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback"
redirect_uri: callbackUrl
]
def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}"
@@ -247,31 +246,26 @@ def getEcobeeThermostats() {
]
def stats = [:]
try {
httpGet(deviceListParams) { resp ->
try {
httpGet(deviceListParams) { resp ->
if (resp.status == 200) {
resp.data.thermostatList.each { stat ->
atomicState.remoteSensors = stat.remoteSensors
def dni = [app.id, stat.identifier].join('.')
stats[dni] = getThermostatDisplayName(stat)
}
} else {
log.debug "http status: ${resp.status}"
//refresh the auth token
if (resp.status == 500 && resp.data.status.code == 14) {
log.debug "Storing the failed action to try later"
atomicState.action = "getEcobeeThermostats"
log.debug "Refreshing your auth_token!"
refreshAuthToken()
} else {
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
}
}
if (resp.status == 200) {
resp.data.thermostatList.each { stat ->
atomicState.remoteSensors = stat.remoteSensors
def dni = [app.id, stat.identifier].join('.')
stats[dni] = getThermostatDisplayName(stat)
}
} else {
log.debug "http status: ${resp.status}"
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.trace "Exception polling children: " + e.response.data.status
if (e.response.data.status.code == 14) {
atomicState.action = "getEcobeeThermostats"
log.debug "Refreshing your auth_token!"
refreshAuthToken()
}
} catch(Exception e) {
log.debug "___exception getEcobeeThermostats(): " + e
refreshAuthToken()
}
atomicState.thermostats = stats
return stats
@@ -317,7 +311,7 @@ def initialize() {
def devices = thermostats.collect { dni ->
def d = getChildDevice(dni)
if(!d) {
d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"Ecobee Thermostat:${atomicState.thermostats[dni]}"])
d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"${atomicState.thermostats[dni]}" ?: "Ecobee Thermostat"])
log.debug "created ${d.displayName} with id $dni"
} else {
log.debug "found ${d.displayName} with id $dni already exists"
@@ -328,7 +322,7 @@ def initialize() {
def sensors = ecobeesensors.collect { dni ->
def d = getChildDevice(dni)
if(!d) {
d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"Ecobee Sensor:${atomicState.sensors[dni]}"])
d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"${atomicState.sensors[dni]}" ?:"Ecobee Sensor"])
log.debug "created ${d.displayName} with id $dni"
} else {
log.debug "found ${d.displayName} with id $dni already exists"
@@ -354,21 +348,17 @@ def initialize() {
atomicState.thermostatData = [:] //reset Map to store thermostat data
//send activity feeds to tell that device is connected
def notificationMessage = "is connected to SmartThings"
sendActivityFeeds(notificationMessage)
state.timeSendPush = null
//send activity feeds to tell that device is connected
def notificationMessage = "is connected to SmartThings"
sendActivityFeeds(notificationMessage)
atomicState.timeSendPush = null
atomicState.reAttempt = 0
pollHandler() //first time polling data data from thermostat
//automatically update devices status every 5 mins
runEvery5Minutes("poll")
//since access_token expires every 2 hours
runEvery1Hour("refreshAuthToken")
atomicState.reAttempt = 0
}
def pollHandler() {
@@ -389,18 +379,10 @@ def pollHandler() {
def pollChildren(child = null) {
def thermostatIdsString = getChildDeviceIdsString()
log.debug "polling children: $thermostatIdsString"
def data = ""
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}'
def result = false
// // TODO: test this:
//
// def jsonRequestBody = toJson([
// selection:[
// selectionType: "thermostats",
// selectionMatch: getChildDeviceIdsString(),
// includeRuntime: true
// ]
// ])
def pollParams = [
uri: apiEndpoint,
@@ -411,11 +393,6 @@ def pollChildren(child = null) {
try{
httpGet(pollParams) { resp ->
// if (resp.data) {
// debugEventFromParent(child, "pollChildren(child) >> resp.status = ${resp.status}, resp.data = ${resp.data}")
// }
if(resp.status == 200) {
log.debug "poll results returned resp.data ${resp.data}"
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
@@ -426,45 +403,57 @@ def pollChildren(child = null) {
log.debug "updating dni $dni"
def data = [
data = [
coolMode: (stat.settings.coolStages > 0),
heatMode: (stat.settings.heatStages > 0),
deviceTemperatureUnit: stat.settings.useCelsius,
minHeatingSetpoint: (stat.settings.heatRangeLow / 10),
maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10),
minCoolingSetpoint: (stat.settings.coolRangeLow / 10),
maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10),
autoMode: stat.settings.autoHeatCoolFeatureEnabled,
auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler),
temperature: stat.runtime.actualTemperature / 10,
temperature: (stat.runtime.actualTemperature / 10),
heatingSetpoint: stat.runtime.desiredHeat / 10,
coolingSetpoint: stat.runtime.desiredCool / 10,
thermostatMode: stat.settings.hvacMode
thermostatMode: stat.settings.hvacMode,
humidity: stat.runtime.actualHumidity,
thermostatFanMode: stat.runtime.desiredFanMode
]
data["temperature"] = data["temperature"] ? data["temperature"].toDouble().toInteger() : data["temperature"]
data["heatingSetpoint"] = data["heatingSetpoint"] ? data["heatingSetpoint"].toDouble().toInteger() : data["heatingSetpoint"]
data["coolingSetpoint"] = data["coolingSetpoint"] ? data["coolingSetpoint"].toDouble().toInteger() : data["coolingSetpoint"]
// debugEventFromParent(child, "Event Data = ${data}")
if (location.temperatureScale == "F")
{
data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"]
data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"]
data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"]
data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"]
data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"]
data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"]
data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"]
}
if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") {
data["deviceTemperatureUnit"] = "F"
} else {
data["deviceTemperatureUnit"] = "C"
}
collector[dni] = [data:data]
return collector
}
result = true
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
} else {
log.error "polling children & got http status ${resp.status}"
//refresh the auth token
if (resp.status == 500 && resp.data.status.code == 14) {
log.debug "Storing the failed action to try later"
atomicState.action = "pollChildren";
log.debug "Refreshing your auth_token!"
refreshAuthToken()
}
else {
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
}
}
}
} catch(Exception e) {
log.debug "___exception polling children: " + e
// debugEventFromParent(child, "___exception polling children: " + e)
refreshAuthToken()
} catch (groovyx.net.http.HttpResponseException e) {
log.trace "Exception polling children: " + e.response.data.status
if (e.response.data.status.code == 14) {
atomicState.action = "pollChildren"
log.debug "Refreshing your auth_token!"
refreshAuthToken()
}
}
return result
}
@@ -476,18 +465,14 @@ def pollChild(child){
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
def tData = atomicState.thermostats[child.device.deviceNetworkId]
// debugEventFromParent(child, "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}") //TODO comment
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
child.generateEvent(tData.data) //parse received message from parent
// return tData.data
} else if(atomicState.thermostats[child.device.deviceNetworkId] == null) {
// debugEventFromParent(child, "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling") //TODO comment
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}"
return null
}
}
} else {
// debugEventFromParent(child, "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling") //TODO comment
log.info "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling"
return null
}
@@ -513,9 +498,6 @@ def availableModes(child) {
{
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
// TODO: flag device as in error state
// child.errorState = true
return null
}
@@ -542,8 +524,6 @@ def currentMode(child) {
if(!tData) {
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
// TODO: flag device as in error state
// child.errorState = true
return null
}
@@ -561,8 +541,12 @@ def updateSensorData() {
def occupancy = ""
it.capability.each {
if (it.type == "temperature") {
temperature = it.value as Double
temperature = (temperature / 10).toInteger()
if (location.temperatureScale == "F") {
temperature = Math.round(it.value.toDouble() / 10)
} else {
temperature = convertFtoC(it.value.toDouble() / 10)
}
} else if (it.type == "occupancy") {
if(it.value == "true")
occupancy = "active"
@@ -575,7 +559,6 @@ def updateSensorData() {
if(d) {
d.sendEvent(name:"temperature", value: temperature)
d.sendEvent(name:"motion", value: occupancy)
// debugEventFromParent(d, "temperature : ${temperature}, motion:${occupancy}")
}
}
}
@@ -595,68 +578,65 @@ def toQueryString(Map m) {
}
private refreshAuthToken() {
log.debug "refreshing auth token"
log.debug "refreshing auth token"
if(!atomicState.refreshToken) {
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
} else {
if(!atomicState.refreshToken) {
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
} else {
def refreshParams = [
method: 'POST',
uri : apiEndpoint,
path : "/token",
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
]
def refreshParams = [
method: 'POST',
uri : apiEndpoint,
path : "/token",
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
]
log.debug refreshParams
log.debug refreshParams
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
//changed to httpPost
try {
def jsonMap
httpPost(refreshParams) { resp ->
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
//changed to httpPost
try {
def jsonMap
httpPost(refreshParams) { resp ->
if(resp.status == 200) {
log.debug "Token refreshed...calling saved RestAction now!"
if(resp.status == 200) {
log.debug "Token refreshed...calling saved RestAction now!"
debugEvent("Token refreshed ... calling saved RestAction now!")
debugEvent("Token refreshed ... calling saved RestAction now!")
log.debug resp
log.debug resp
jsonMap = resp.data
jsonMap = resp.data
if(resp.data) {
if(resp.data) {
log.debug resp.data
debugEvent("Response = ${resp.data}")
log.debug resp.data
debugEvent("Response = ${resp.data}")
atomicState.refreshToken = resp?.data?.refresh_token
atomicState.authToken = resp?.data?.access_token
atomicState.refreshToken = resp?.data?.refresh_token
atomicState.authToken = resp?.data?.access_token
debugEvent("Refresh Token = ${atomicState.refreshToken}")
debugEvent("OAUTH Token = ${atomicState.authToken}")
debugEvent("Refresh Token = ${atomicState.refreshToken}")
debugEvent("OAUTH Token = ${atomicState.authToken}")
if(atomicState.action && atomicState.action != "") {
log.debug "Executing next action: ${atomicState.action}"
if(atomicState.action && atomicState.action != "") {
log.debug "Executing next action: ${atomicState.action}"
"${atomicState.action}"()
"${atomicState.action}"()
//remove saved action
atomicState.action = ""
}
atomicState.action = ""
}
}
atomicState.action = ""
} else {
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
}
}
} catch(Exception e) {
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
}
atomicState.action = ""
}
}
} catch (groovyx.net.http.HttpResponseException e) {
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
def reAttemptPeriod = 300 // in sec
if (e.statusCode != 401) { //this issue might comes from exceed 20sec app execution, connectivity issue etc.
if (e.statusCode != 401) { // this issue might comes from exceed 20sec app execution, connectivity issue etc.
runIn(reAttemptPeriod, "refreshAuthToken")
} else if (e.statusCode == 401) { //refresh token is expired
} else if (e.statusCode == 401) { // unauthorized
atomicState.reAttempt = atomicState.reAttempt + 1
log.warn "reAttempt refreshAuthToken to try = ${atomicState.reAttempt}"
if (atomicState.reAttempt <= 3) {
@@ -665,20 +645,16 @@ private refreshAuthToken() {
sendPushAndFeeds(notificationMessage)
atomicState.reAttempt = 0
}
}
}
}
}
}
}
}
def resumeProgram(child, deviceId) {
// def thermostatIdsString = getChildDeviceIdsString()
// log.debug "resumeProgram children: $thermostatIdsString"
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}'
//, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } }
def result = sendJson(jsonRequestBody)
// debugEventFromParent(child, "resumeProgram(child) with result ${result}")
return result
}
@@ -686,28 +662,27 @@ def setHold(child, heating, cooling, deviceId, sendHoldType) {
int h = heating * 10
int c = cooling * 10
// log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c"
// def thermostatIdsString = getChildDeviceIdsString()
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}'
// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}'
def result = sendJson(child, jsonRequestBody)
return result
}
def setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) {
int h = heating * 10
int c = cooling * 10
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+', "fan": '+fanMode+' } } ]}'
def result = sendJson(child, jsonRequestBody)
// debugEventFromParent(child, "setHold: heating: ${h}, cooling: ${c} with result ${result}")
return result
}
def setMode(child, mode, deviceId) {
// def thermostatIdsString = getChildDeviceIdsString()
// log.debug "setCoolingSetpoint children: $thermostatIdsString"
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}'
// log.debug "Mode Request Body = ${jsonRequestBody}"
// debugEvent ("Mode Request Body = ${jsonRequestBody}")
def result = sendJson(jsonRequestBody)
// debugEventFromParent(child, "setMode to ${mode} with result ${result}")
return result
}
@@ -724,8 +699,6 @@ def sendJson(child = null, String jsonBody) {
try{
httpPost(cmdParams) { resp ->
// debugEventFromParent(child, "sendJson >> resp.status ${resp.status}, resp.data: ${resp.data}")
if(resp.status == 200) {
log.debug "updated ${resp.data}"
@@ -736,30 +709,21 @@ def sendJson(child = null, String jsonBody) {
log.debug "Error return code = ${resp.data.status.code}"
debugEvent("Error return code = ${resp.data.status.code}")
}
} else {
log.error "sent Json & got http status ${resp.status} - ${resp.status.code}"
debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}")
//refresh the auth token
if (resp.status == 500 && resp.status.code == 14) {
//log.debug "Storing the failed action to try later"
log.debug "Refreshing your auth_token!"
debugEvent ("Refreshing OAUTH Token")
refreshAuthToken()
return false
} else {
debugEvent ("Authentication error, invalid authentication method, lack of credentials, etc.")
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
return false
}
}
}
} catch(Exception e) {
log.debug "Exception Sending Json: " + e
debugEvent ("Exception Sending JSON: " + e)
refreshAuthToken()
return false
}
} catch (groovyx.net.http.HttpResponseException e) {
log.trace "Exception Sending Json: " + e.response.data.status
debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
if (e.response.data.status.code == 14) {
atomicState.action = "pollChildren"
log.debug "Refreshing your auth_token!"
refreshAuthToken()
}
else {
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
}
}
if (returnStatus == 0)
return true
@@ -794,25 +758,37 @@ def debugEventFromParent(child, message) {
//send both push notification and mobile activity feeds
def sendPushAndFeeds(notificationMessage){
log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}"
log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}"
if (atomicState.timeSendPush){
if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day
sendPush("Your Ecobee thermostat " + notificationMessage)
sendActivityFeeds(notificationMessage)
atomicState.timeSendPush = now()
}
} else {
sendPush("Your Ecobee thermostat " + notificationMessage)
sendActivityFeeds(notificationMessage)
atomicState.timeSendPush = now()
}
atomicState.authToken = null
log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}"
log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}"
if (atomicState.timeSendPush){
if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day
sendPush("Your Ecobee thermostat " + notificationMessage)
sendActivityFeeds(notificationMessage)
atomicState.timeSendPush = now()
}
} else {
sendPush("Your Ecobee thermostat " + notificationMessage)
sendActivityFeeds(notificationMessage)
atomicState.timeSendPush = now()
}
atomicState.authToken = null
}
def sendActivityFeeds(notificationMessage) {
def devices = getChildDevices()
devices.each { child ->
child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent
}
def devices = getChildDevices()
devices.each { child ->
child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent
}
}
def roundC (tempC) {
return String.format("%.1f", (Math.round(tempC * 2))/2)
}
def convertFtoC (tempF) {
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
}
def convertCtoF (tempC) {
return (Math.round(tempC * (9/5)) + 32).toInteger()
}

View File

@@ -201,8 +201,8 @@ def completionPage() {
section("Switches") {
input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true)
if (completionSwitches || androidClient()) {
input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on")
if (completionSwitches) {
input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on")
input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)")
}
}

View File

@@ -35,23 +35,11 @@ preferences {
}
def mainPage() {
if(canInstallLabs()) {
def bridges = bridgesDiscovered()
if (state.username && bridges) {
return bulbDiscovery()
} else {
return bridgeDiscovery()
}
def bridges = bridgesDiscovered()
if (state.username && bridges) {
return bulbDiscovery()
} else {
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
section("Upgrade") {
paragraph "$upgradeNeeded"
}
}
return bridgeDiscovery()
}
}
@@ -765,10 +753,6 @@ private String convertHexToIP(hex) {
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}
private Boolean canInstallLabs() {
return hasAllHubsOver("000.011.00603")
}
private Boolean hasAllHubsOver(String desiredFirmware) {
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
}

View File

@@ -419,9 +419,11 @@ def addDevice() {
def d = getChildDevice(dni)
if(!d) {
def newAction = state.HarmonyActivities.find { it.key == dni }
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
log.trace "created ${d.displayName} with id $dni"
poll()
if (newAction) {
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
log.trace "created ${d.displayName} with id $dni"
poll()
}
} else {
log.trace "found ${d.displayName} with id $dni already exists"
}

View File

@@ -0,0 +1,31 @@
'''Acceleration Detected'''.ko=가속화 감지됨
'''Arrival Of'''.ko=도착
'''Both Push and SMS?'''.ko=푸시 메시지와 SMS를 모두 사용하시겠습니까?
'''Button Pushed'''.ko=버튼이 눌렸습니다
'''Contact Closes'''.ko=접점 닫힘
'''Contact Opens'''.ko=접점 열림
'''Departure Of'''.ko=출발
'''Message Text'''.ko=문자 메시지
'''Minutes'''.ko=
'''Motion Here'''.ko=동작
'''Phone Number (for SMS, optional)'''.ko=휴대전화 번호(문자 메시지 - 옵션)
'''Receive notifications when anything happens in your home.'''.ko=집 안에 무슨 일이 일어나면 알림이 전송됩니다.
'''Smoke Detected'''.ko=연기가 감지되었습니다
'''Switch Turned Off'''.ko=스위치 꺼짐
'''Switch Turned On'''.ko=스위치 꺼짐
'''Choose one or more, when...'''.ko=다음의 경우 하나 이상 선택
'''Yes'''.ko=
'''No'''.ko=아니요
'''Send this message (optional, sends standard status message if not specified)'''.ko=이 메시지 전송(선택적, 지정되지 않은 경우 표준 상태 메시지를 보냅니다)
'''Via a push notification and/or an SMS message'''.ko=푸시 알림 및/또는 문자 메시지를 통해
'''Set for specific mode(s)'''.ko=특정 모드 설정
'''Tap to set'''.ko=눌러서 설정
'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지작 간 최소 시간(선택 사항, 모든 메시지의 기본 설정)
'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 거주자는 적절한 국가 코드를 입력했는지 확인하십시오
'''Water Sensor Wet'''.ko=Water Sensor에서 물이 감지되었습니다
'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다
'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다
'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다
'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다
'''Assign a name'''.ko=이름 배정
'''Choose Modes'''.ko=모드 선택

View File

@@ -20,19 +20,19 @@
* 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell.
*/
definition(
name: "Notify Me When",
namespace: "smartthings",
author: "SmartThings",
description: "Receive notifications when anything happens in your home.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
name: "Notify Me When",
namespace: "smartthings",
author: "SmartThings",
description: "Receive notifications when anything happens in your home.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png"
)
preferences {
section("Choose one or more, when..."){
input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
@@ -47,11 +47,11 @@ preferences {
input "messageText", "text", title: "Message Text", required: false
}
section("Via a push notification and/or an SMS message"){
input("recipients", "contact", title: "Send notifications to") {
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("recipients", "contact", title: "Send notifications to") {
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"]
}
}
section("Minimum time between messages (optional, defaults to every message)") {
input "frequency", "decimal", title: "Minutes", required: false
@@ -71,7 +71,7 @@ def updated() {
def subscribeToEvents() {
subscribe(button, "button.pushed", eventHandler) //tw
subscribe(contact, "contact.open", eventHandler)
subscribe(contact, "contact.open", eventHandler)
subscribe(contactClosed, "contact.closed", eventHandler)
subscribe(acceleration, "acceleration.active", eventHandler)
subscribe(motion, "motion.active", eventHandler)
@@ -99,49 +99,55 @@ def eventHandler(evt) {
}
private sendMessage(evt) {
def msg = messageText ?: defaultText(evt)
String msg = messageText
Map options = [:]
if (!messageText) {
msg = defaultText(evt)
options = [translatable: true, triggerEvent: evt]
}
log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'"
if (location.contactBookEnabled) {
sendNotificationToContacts(msg, recipients)
}
else {
if (location.contactBookEnabled) {
sendNotificationToContacts(msg, recipients, options)
} else {
if (!phone || pushAndPhone != 'No') {
log.debug 'sending push'
options.method = 'push'
//sendPush(msg)
}
if (phone) {
options.phone = phone
log.debug 'sending SMS'
//sendSms(phone, msg)
}
sendNotification(msg, options)
}
if (!phone || pushAndPhone != "No") {
log.debug "sending push"
sendPush(msg)
}
if (phone) {
log.debug "sending SMS"
sendSms(phone, msg)
}
}
if (frequency) {
state[evt.deviceId] = now()
}
}
private defaultText(evt) {
if (evt.name == "presence") {
if (evt.value == "present") {
if (evt.name == 'presence') {
if (evt.value == 'present') {
if (includeArticle) {
"$evt.linkText has arrived at the $location.name"
'{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'
}
else {
"$evt.linkText has arrived at $location.name"
'{{ triggerEvent.linkText }} has arrived at {{ location.name }}'
}
}
else {
} else {
if (includeArticle) {
"$evt.linkText has left the $location.name"
'{{ triggerEvent.linkText }} has left the {{ location.name }}'
}
else {
"$evt.linkText has left $location.name"
'{{ triggerEvent.linkText }} has left {{ location.name }}'
}
}
}
else {
evt.descriptionText
} else {
'{{ triggerEvent.descriptionText }}'
}
}

View File

@@ -16,14 +16,14 @@
* Date: 2013-09-06
*/
definition(
name: "Wemo (Connect)",
namespace: "smartthings",
author: "SmartThings",
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png",
singleInstance: true
name: "Wemo (Connect)",
namespace: "smartthings",
author: "SmartThings",
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
category: "SmartThings Labs",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png",
singleInstance: true
)
preferences {
@@ -39,7 +39,7 @@ private getFriendlyName(String deviceNetworkId) {
sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1
HOST: ${deviceNetworkId}
""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}"))
""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}", [callback: "setupHandler"]))
}
private verifyDevices() {
@@ -52,6 +52,13 @@ private verifyDevices() {
}
}
void ssdpSubscribe() {
subscribe(location, "ssdpTerm.urn:Belkin:device:insight:1", ssdpSwitchHandler)
subscribe(location, "ssdpTerm.urn:Belkin:device:controllee:1", ssdpSwitchHandler)
subscribe(location, "ssdpTerm.urn:Belkin:device:sensor:1", ssdpMotionHandler)
subscribe(location, "ssdpTerm.urn:Belkin:device:lightswitch:1", ssdpLightSwitchHandler)
}
def firstPage()
{
if(canInstallLabs())
@@ -62,7 +69,7 @@ def firstPage()
log.debug "REFRESH COUNT :: ${refreshCount}"
subscribe(location, null, locationHandler, [filterEvents:false])
ssdpSubscribe()
//ssdp request every 25 seconds
if((refreshCount % 5) == 0) {
@@ -105,9 +112,7 @@ def devicesDiscovered() {
def motions = getWemoMotions()
def lightSwitches = getWemoLightSwitches()
def devices = switches + motions + lightSwitches
def list = []
list = devices?.collect{ [app.id, it.ssdpUSN].join('.') }
devices?.collect{ [app.id, it.ssdpUSN].join('.') }
}
def switchesDiscovered() {
@@ -175,8 +180,9 @@ def updated() {
def initialize() {
unsubscribe()
unschedule()
subscribe(location, null, locationHandler, [filterEvents:false])
unschedule()
ssdpSubscribe()
if (selectedSwitches)
addSwitches()
@@ -189,7 +195,7 @@ def initialize() {
runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
runEvery5Minutes("refresh")
runEvery5Minutes("refresh")
}
def resubscribe() {
@@ -199,7 +205,7 @@ def resubscribe() {
def refresh() {
log.debug "refresh() called"
doDeviceSync()
doDeviceSync()
refreshDevices()
}
@@ -235,14 +241,14 @@ def addSwitches() {
if (!d) {
log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}"
d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [
"label": selectedSwitch?.value?.name ?: "Wemo Switch",
"data": [
"mac": selectedSwitch.value.mac,
"ip": selectedSwitch.value.ip,
"port": selectedSwitch.value.port
]
"label": selectedSwitch?.value?.name ?: "Wemo Switch",
"data": [
"mac": selectedSwitch.value.mac,
"ip": selectedSwitch.value.ip,
"port": selectedSwitch.value.port
]
])
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
} else {
@@ -273,9 +279,9 @@ def addMotions() {
"port": selectedMotion.value.port
]
])
def ipvalue = convertHexToIP(selectedMotion.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
def ipvalue = convertHexToIP(selectedMotion.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
} else {
log.debug "found ${d.displayName} with id $dni already exists"
}
@@ -304,26 +310,147 @@ def addLightSwitches() {
"port": selectedLightSwitch.value.port
]
])
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
log.debug "created ${d.displayName} with id $dni"
} else {
log.debug "found ${d.displayName} with id $dni already exists"
log.debug "found ${d.displayName} with id $dni already exists"
}
}
}
def ssdpSwitchHandler(evt) {
def description = evt.description
def hub = evt?.hubId
def parsedEvent = parseDiscoveryMessage(description)
parsedEvent << ["hub":hub]
log.debug parsedEvent
def switches = getWemoSwitches()
if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
//if it doesn't already exist
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else {
log.debug "Device was already found in state..."
def d = switches."${parsedEvent.ssdpUSN.toString()}"
boolean deviceChangedValues = false
log.debug "$d.ip <==> $parsedEvent.ip"
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
d.ip = parsedEvent.ip
d.port = parsedEvent.port
deviceChangedValues = true
log.debug "Device's port or ip changed..."
def child = getChildDevice(parsedEvent.mac)
child.subscribe(parsedEvent.ip, parsedEvent.port)
child.poll()
}
}
}
def ssdpMotionHandler(evt) {
log.info("ssdpMotionHandler")
def description = evt.description
def hub = evt?.hubId
def parsedEvent = parseDiscoveryMessage(description)
parsedEvent << ["hub":hub]
log.debug parsedEvent
def motions = getWemoMotions()
if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
//if it doesn't already exist
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else { // just update the values
log.debug "Device was already found in state..."
def d = motions."${parsedEvent.ssdpUSN.toString()}"
boolean deviceChangedValues = false
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
d.ip = parsedEvent.ip
d.port = parsedEvent.port
deviceChangedValues = true
log.debug "Device's port or ip changed..."
}
if (deviceChangedValues) {
def children = getChildDevices()
log.debug "Found children ${children}"
children.each {
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
it.subscribe(parsedEvent.ip, parsedEvent.port)
}
}
}
}
}
def ssdpLightSwitchHandler(evt) {
log.info("ssdpLightSwitchHandler")
def description = evt.description
def hub = evt?.hubId
def parsedEvent = parseDiscoveryMessage(description)
parsedEvent << ["hub":hub]
log.debug parsedEvent
def lightSwitches = getWemoLightSwitches()
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) {
//if it doesn't already exist
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else {
log.debug "Device was already found in state..."
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
boolean deviceChangedValues = false
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
d.ip = parsedEvent.ip
d.port = parsedEvent.port
deviceChangedValues = true
log.debug "Device's port or ip changed..."
def child = getChildDevice(parsedEvent.mac)
log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}"
child.subscribe(parsedEvent.ip, parsedEvent.port)
}
}
}
void setupHandler(hubResponse) {
String contentType = hubResponse?.headers['Content-Type']
if (contentType != null && contentType == 'text/xml') {
def body = hubResponse.xml
def wemoDevices = []
String deviceType = body?.device?.deviceType?.text() ?: ""
if (deviceType.startsWith("urn:Belkin:device:controllee:1") || deviceType.startsWith("urn:Belkin:device:insight:1")) {
wemoDevices = getWemoSwitches()
} else if (deviceType.startsWith("urn:Belkin:device:sensor")) {
wemoDevices = getWemoMotions()
} else if (deviceType.startsWith("urn:Belkin:device:lightswitch")) {
wemoDevices = getWemoLightSwitches()
}
def wemoDevice = wemoDevices.find {it?.key?.contains(body?.device?.UDN?.text())}
if (wemoDevice) {
wemoDevice.value << [name:body?.device?.friendlyName?.text(), verified: true]
} else {
log.error "/setup.xml returned a wemo device that didn't exist"
}
}
}
@Deprecated
def locationHandler(evt) {
def description = evt.description
def hub = evt?.hubId
def parsedEvent = parseDiscoveryMessage(description)
parsedEvent << ["hub":hub]
log.debug parsedEvent
log.debug parsedEvent
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
def switches = getWemoSwitches()
if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
//if it doesn't already exist
//if it doesn't already exist
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else {
log.debug "Device was already found in state..."
@@ -335,16 +462,16 @@ def locationHandler(evt) {
d.port = parsedEvent.port
deviceChangedValues = true
log.debug "Device's port or ip changed..."
def child = getChildDevice(parsedEvent.mac)
def child = getChildDevice(parsedEvent.mac)
child.subscribe(parsedEvent.ip, parsedEvent.port)
child.poll()
child.poll()
}
}
}
else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
def motions = getWemoMotions()
if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
//if it doesn't already exist
//if it doesn't already exist
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
} else { // just update the values
log.debug "Device was already found in state..."
@@ -459,6 +586,7 @@ def locationHandler(evt) {
}
}
@Deprecated
private def parseXmlBody(def body) {
def decodedBytes = body.decodeBase64()
def bodyString
@@ -540,4 +668,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) {
private List getRealHubFirmwareVersions() {
return location.hubs*.firmwareVersionString.findAll { it }
}
}