Compare commits

..

5 Commits

Author SHA1 Message Date
Mattias Svännel
6880817cc6 MSA-2000: ClassicGOD:Fibaro Wall Plug ZW5 2017-05-23 10:34:23 -07:00
Duncan McKee
10964873cd Merge pull request #2001 from mckeed/DVCSMP-2493
DVCSMP-2493 Notify Multi-Channel Control app when root device is deleted
2017-05-23 13:33:55 -04:00
Zach Varberg
20f086fe69 Merge pull request #2023 from varzac/fix-multisensor-npe
[DVCSMP-2668] Fix NPE for smartsense multi
2017-05-23 09:08:47 -05:00
Zach Varberg
97c9ec7a95 Fix NPE for smartsense multi
There was a common NPE caused by assuming there would be additional
attributes reported with one acceleration value.  This corrects it to
instead properly handle the case where there weren't additional
attributes.

This resolves: https://smartthings.atlassian.net/browse/DVCSMP-2668
2017-05-22 11:52:13 -05:00
Duncan McKee
52c57f66fb DVCSMP-2493 Notify Multi-Channel Control app when root device is deleted 2017-03-06 13:31:32 -05:00
4 changed files with 373 additions and 795 deletions

View File

@@ -0,0 +1,368 @@
/**
* Fibaro Wall Plug ZW5
* Requires: Fibaro Double Switch 2 Child Device
*
* Copyright 2017 Artur Draga
*
* 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: "Fibaro Wall Plug ZW5", namespace: "ClassicGOD", author: "Artur Draga") {
capability "Switch"
capability "Energy Meter"
capability "Power Meter"
capability "Configuration"
capability "Health Check"
command "reset"
command "refresh"
fingerprint deviceId: "0x1001", inClusters:"0x5E,0x22,0x59,0x56,0x7A,0x32,0x71,0x73,0x98,0x31,0x85,0x70,0x72,0x5A,0x8E,0x25,0x86"
fingerprint deviceId: "0x1001", inClusters:"0x5E,0x22,0x59,0x56,0x7A,0x32,0x71,0x73,0x31,0x85,0x70,0x72,0x5A,0x8E,0x25,0x86"
}
tiles (scale: 2) {
multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4, canChangeIcon: true){
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState:"turningOn"
attributeState "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState:"turningOff"
attributeState "turningOn", label:'Turning On', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff"
attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
}
tileAttribute("device.combinedMeter", key:"SECONDARY_CONTROL") {
attributeState("combinedMeter", label:'${currentValue}')
}
}
valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) {
state "power", label:'${currentValue}\nW', action:"refresh"
}
valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) {
state "energy", label:'${currentValue}\nkWh', action:"refresh"
}
valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) {
state "reset", label:'reset\nkWh', action:"reset"
}
}
preferences {
input ( name: "logging", title: "Logging", type: "boolean", required: false )
parameterMap().each {
input (
name: it.key,
title: "${it.num}. ${it.title}",
description: it.descr,
type: it.type,
options: it.options,
range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null,
defaultValue: it.def,
required: false
)
}
}
}
//UI and tile functions
def on() {
encap(zwave.basicV1.basicSet(value: 255))
}
def off() {
encap(zwave.basicV1.basicSet(value: 0))
}
def reset() {
def cmds = []
cmds << zwave.meterV3.meterReset()
cmds << zwave.meterV3.meterGet(scale: 0)
cmds << zwave.meterV3.meterGet(scale: 2)
encapSequence(cmds,1000)
}
def refresh() {
def cmds = []
cmds << zwave.meterV3.meterGet(scale: 0)
cmds << zwave.meterV3.meterGet(scale: 2)
cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 4)
encapSequence(cmds,1000)
}
//Configuration and synchronization
def updated() {
if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return
def cmds = []
logging("${device.displayName} - Executing updated()","info")
def Integer cmdCount = 0
parameterMap().each {
if(settings."$it.key" != null) {
if (state."$it.key" == null) { state."$it.key" = [value: null, state: "synced"] }
if (state."$it.key".value != settings."$it.key" as Integer || state."$it.key".state == "notSynced") {
state."$it.key".value = settings."$it.key" as Integer
state."$it.key".state = "notSynced"
cmds << zwave.configurationV2.configurationSet(configurationValue: intToParam(state."$it.key".value, it.size), parameterNumber: it.num, size: it.size)
cmds << zwave.configurationV2.configurationGet(parameterNumber: it.num)
cmdCount = cmdCount + 1
}
}
}
if ( cmdCount > 0 ) {
logging("${device.displayName} - sending config.", "info")
sendEvent(name: "combinedMeter", value: "SYNC IN PROGRESS.", displayed: false)
runIn((5+cmdCount*2), syncCheck)
}
state.lastUpdated = now()
if (cmds) { response(encapSequence(cmds,1000)) }
}
def syncCheck() {
logging("${device.displayName} - Executing syncCheck()","info")
def Integer count = 0
if (device.currentValue("combinedMeter")?.contains("SYNC") && device.currentValue("combinedMeter") != "SYNC OK.") {
parameterMap().each {
if (state."$it.key".state == "notSynced" ) {
count = count + 1
}
}
}
if (count == 0) {
logging("${device.displayName} - Sync Complete","info")
sendEvent(name: "combinedMeter", value: "SYNC OK.", displayed: false)
} else {
logging("${device.displayName} Sync Incomplete","info")
if (device.currentValue("combinedMeter") != "SYNC FAILED!") {
sendEvent(name: "combinedMeter", value: "SYNC INCOMPLETE.", displayed: false)
}
}
}
//event handlers related to configuration and sync
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
def paramKey = parameterMap().find( {it.num == cmd.parameterNumber } ).key
logging("${device.displayName} - Parameter ${paramKey} value is ${cmd.scaledConfigurationValue} expected " + state."$paramKey".value, "info")
if (state."$paramKey".value == cmd.scaledConfigurationValue) {
state."$paramKey".state = "synced"
}
}
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
logging("${device.displayName} - rejected request!","warn")
if (device.currentValue("combinedMeter") == "SYNC IN PROGRESS.") {
sendEvent(name: "combinedMeter", value: "SYNC FAILED!", displayed: false)
}
}
//event handlers
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
//ignore
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
logging("${device.displayName} - SwitchBinaryReport received, value: ${cmd.value}","info")
sendEvent([name: "switch", value: (cmd.value == 0 ) ? "off": "on"])
}
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
logging("${device.displayName} - SensorMultilevelReport received, value: ${cmd.scaledSensorValue} scale: ${cmd.scale}","info")
if (cmd.sensorType == 4) {
sendEvent([name: "power", value: cmd.scaledSensorValue, unit: "W"])
updateCombinedMeter()
}
}
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {
logging("${device.displayName} - MeterReport received, value: ${cmd.scaledMeterValue} scale: ${cmd.scale}","info")
switch (cmd.scale) {
case 0:
sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"])
break
case 2:
sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"])
break
}
updateCombinedMeter()
}
//other
private updateCombinedMeter() {
if (!device.currentValue("combinedMeter")?.contains("SYNC") || device.currentValue("combinedMeter") == "SYNC OK." || device.currentValue("combinedMeter") == null ) {
sendEvent([name: "combinedMeter", value: "${device.currentValue("power")} W / ${device.currentValue("energy")} kWh", displayed: false])
}
}
/*
####################
## Z-Wave Toolkit ##
####################
*/
def parse(String description) {
def result = []
logging("${device.displayName} - Parsing: ${description}")
if (description.startsWith("Err 106")) {
result = createEvent(
descriptionText: "Failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.",
eventType: "ALERT",
name: "secureInclusion",
value: "failed",
displayed: true,
)
} else if (description == "updated") {
return null
} else {
def cmd = zwave.parse(description, cmdVersions())
if (cmd) {
logging("${device.displayName} - Parsed: ${cmd}")
zwaveEvent(cmd)
}
}
}
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions())
if (encapsulatedCommand) {
logging("${device.displayName} - Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}")
zwaveEvent(encapsulatedCommand)
} else {
log.warn "Unable to extract secure cmd from $cmd"
createEvent(descriptionText: cmd.toString())
}
}
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {
def version = cmdVersions()[cmd.commandClass as Integer]
def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
if (!encapsulatedCommand) {
log.warn "Could not extract crc16 command from $cmd"
} else {
logging("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}")
zwaveEvent(encapsulatedCommand)
}
}
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions())
if (encapsulatedCommand) {
logging("${device.displayName} - Parsed MultiChannelCmdEncap ${encapsulatedCommand}")
zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer)
}
}
private logging(text, type = "debug") {
if (settings.logging == "true") {
log."$type" text
}
}
private secEncap(physicalgraph.zwave.Command cmd) {
logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","info")
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
}
private crcEncap(physicalgraph.zwave.Command cmd) {
logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","info")
zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() // doesn't work righ now because SmartThings...
//"5601${cmd.format()}0000"
}
private multiEncap(physicalgraph.zwave.Command cmd, Integer ep) {
logging("${device.displayName} - encapsulating command using Multi Channel Encapsulation, ep: $ep command: $cmd","info")
zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:ep).encapsulate(cmd)
}
private encap(physicalgraph.zwave.Command cmd) {
if (zwaveInfo.zw.contains("s")) {
secEncap(cmd)
} else if (zwaveInfo.cc.contains("56")){
crcEncap(cmd)
} else {
logging("${device.displayName} - no encapsulation supported for command: $cmd","info")
cmd.format()
}
}
private encap(physicalgraph.zwave.Command cmd, Integer ep) {
encap(multiEncap(cmd, ep))
}
private encap(List encapList) {
encap(encapList[0], encapList[1])
}
private encap(Map encapMap) {
encap(encapMap.cmd, encapMap.ep)
}
private encapSequence(cmds, Integer delay=250) {
delayBetween(cmds.collect{ encap(it) }, delay)
}
private encapSequence(cmds, Integer delay, Integer ep) {
delayBetween(cmds.collect{ encap(it, ep) }, delay)
}
private List intToParam(Long value, Integer size = 1) {
def result = []
size.times {
result = result.plus(0, (value & 0xFF) as Short)
value = (value >> 8)
}
return result
}
/*
##########################
## Device Configuration ##
##########################
*/
private Map cmdVersions() {
//[0x5E: 2, 0x59: 1, 0x80: 1, 0x56: 1, 0x7A: 3, 0x73: 1, 0x98: 1, 0x22: 1, 0x85: 2, 0x5B: 1, 0x70: 2, 0x8E: 2, 0x86: 2, 0x84: 2, 0x75: 2, 0x72: 2] //Fibaro KeyFob
//[0x5E: 2, 0x86: 1, 0x72: 2, 0x59: 1, 0x80: 1, 0x73: 1, 0x56: 1, 0x22: 1, 0x31: 5, 0x98: 1, 0x7A: 3, 0x20: 1, 0x5A: 1, 0x85: 2, 0x84: 2, 0x71: 3, 0x8E: 2, 0x70: 2, 0x30: 1, 0x9C: 1] //Fibaro Motion Sensor ZW5
//[0x5E: 2, 0x86: 1, 0x72: 1, 0x59: 1, 0x73: 1, 0x22: 1, 0x56: 1, 0x32: 3, 0x71: 1, 0x98: 1, 0x7A: 1, 0x25: 1, 0x5A: 1, 0x85: 2, 0x70: 2, 0x8E: 2, 0x60: 3, 0x75: 1, 0x5B: 1] //Fibaro Double Switch 2
[0x5E: 2, 0x22: 1, 0x59: 1, 0x56: 1, 0x7A: 1, 0x32: 3, 0x71: 1, 0x73: 1, 0x98: 1, 0x31: 5, 0x85: 2, 0x70: 2, 0x72: 2, 0x5A: 1, 0x8E: 2, 0x25: 1, 0x86: 2] //Fibaro Wall Plug ZW5
}
private parameterMap() {[
[key: "alwaysActive", num: 1, size: 1, type: "enum", options: [0: "0 - inactive", 1: "1 - activated"], def: "0", title: "Always on function", descr: null],
[key: "restoreState", num: 2, size: 1, type: "enum", options: [0: "0 - power off after power failure", 1: "1 - restore state"], def: "1", title: "Restore state after power failure", descr: null],
[key: "overloadSafety", num: 3, size: 2, type: "number", def: 0, min: 0, max: 30000 , title: "Overload safety switch", descr: null],
[key: "immediatePowerReports", num: 10, size: 1, type: "number", def: 80, min: 1, max: 100, title: "Immediate power reports", descr: null],
[key: "standardPowerReports", num: 11, size: 1, type: "number", def: 15, min: 1, max: 100, title: "Standard power reports", descr: null],
[key: "powerReportFrequency", num: 12, size: 2, type: "number", def: 30, min: 5, max: 600, title: "Power reporting interval", descr: null],
[key: "energyReport", num: 13, size: 2, type: "number", def: 10, min: 0, max: 500, title: "Energy reports", descr: null],
[key: "periodicReports", num: 14, size: 2, type: "number", def: 3600, min: 0, max: 32400, title: "Periodic power and energy reports", descr: null],
[key: "deviceEnergyConsumed", num: 15, size: 1, type: "enum", options: [0: "0 - don't measure", 1: "1 - measure"], def: "0", title: "Energy consumed by the device itself", descr: null],
[key: "powerLoad", num: 40, size: 2, type: "number", def: 25000, min: 1000, max: 30000, title: "Power load which makes the LED ring flash violet", descr: null],
[key: "ringColorOn", num: 41, size: 1, type: "enum", options: [
0: "0 - Off",
1: "1 - Load based - continuous",
2: "2 - Load based - steps",
3: "3 - White",
4: "4 - Red",
5: "5 - Green",
6: "6 - Blue",
7: "7 - Yellow",
8: "8 - Cyan",
9: "9 - Magenta"
], def: "1", title: "Ring LED color when on", descr: null],
[key: "ringColorOff", num: 42, size: 1, type: "enum", options: [
0: "0 - Off",
1: "1 - Last measured power",
3: "3 - White",
4: "4 - Red",
5: "5 - Green",
6: "6 - Blue",
7: "7 - Yellow",
8: "8 - Cyan",
9: "9 - Magenta"
], def: "0", title: "Ring LED color when off", descr: null]
]}

View File

@@ -178,7 +178,7 @@ private List<Map> handleAcceleration(descMap) {
result += parseAxis(descMap.additionalAttrs)
}
} else if (descMap.clusterInt == 0xFC02 && descMap.attrInt == 0x0012) {
def addAttrs = descMap.additionalAttrs
def addAttrs = descMap.additionalAttrs ?: []
addAttrs << ["attrInt": descMap.attrInt, "value": descMap.value]
result += parseAxis(addAttrs)
}

View File

@@ -75,6 +75,10 @@ def parse(String description) {
return result
}
def uninstalled() {
sendEvent(name: "epEvent", value: "delete all", isStateChange: true, displayed: false, descriptionText: "Delete endpoint devices")
}
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
[ createEvent(descriptionText: "${device.displayName} woke up", isStateChange:true),
response(["delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) ]

View File

@@ -1,794 +0,0 @@
/**
* Zwave Thermostat Manager
*
* Credits and Kudos: this app is largely based on the more popular Thermostat Director SA by Tim Slagle -
* many thanks to @slagle for his continued support.
* Without his brilliance, this app would not exist!
*
*
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "ZWave Thermostat Manager",
namespace: "BD",
author: "Bobby Dobrescu",
description: "Adjust zwave thermostats based on a temperature range of a specific temperature sensor",
category: "My apps",
iconUrl: "http://icons.iconarchive.com/icons/icons8/windows-8/512/Science-Temperature-icon.png",
iconX2Url: "http://icons.iconarchive.com/icons/icons8/windows-8/512/Science-Temperature-icon.png"
)
preferences {
page name:"pageSetup"
page name:"TemperatureSettings"
page name:"ThermostatandDoors"
page name:"ThermostatAway"
page name:"Settings"
}
// Show setup page
def pageSetup() {
def pageProperties = [
name: "pageSetup",
title: "",
nextPage: null,
install: true,
uninstall: true
]
return dynamicPage(pageProperties) {
section("General Settings") {
href "TemperatureSettings", title: "Ambiance", description: "", state:greyedOut()
href "ThermostatandDoors", title: "Disabled Mode", description: "", state: greyedOutTherm()
href "ThermostatAway", title: "Away Mode", description: "", state: greyedOutTherm2()
href "Settings", title: "Other Settings", description: "", state: greyedOutSettings()
}
section([title:"Options", mobileOnly:true]) {
label title:"Assign a name", required:false
}
}
}
// Page - Temperature Settings
def TemperatureSettings() {
def sensor = [
name: "sensor",
type: "capability.temperatureMeasurement",
title: "Which Temperature Sensor(s)?",
multiple: true,
required: false
]
def thermostat = [
name: "thermostat",
type: "capability.thermostat",
title: "Which Thermostat?",
multiple: false,
required: true
]
def setLow = [
name: "setLow",
type: "number",
title: "Low temp?",
required: true
]
def cold = [
name: "cold",
type: "enum",
title: "Mode?",
required: false,
metadata: [values:["auto", "heat", "cool", "off"]]
]
def SetHeatingLow = [
name: "SetHeatingLow",
type: "number",
title: "Heating Temperature (degrees)",
required: true
]
def SetCoolingLow = [
name: "SetCoolingLow",
type: "number",
title: "Cooling Temperature (degrees)",
required: false
]
def setHigh = [
name: "setHigh",
type: "number",
title: "High temp?",
required: true
]
def hot = [
name: "hot",
type: "enum",
title: "Mode?",
required: false,
metadata: [values:["auto", "heat", "cool", "off"]]
]
def SetHeatingHigh = [
name: "SetHeatingHigh",
type: "number",
title: "Heating Temperature (degrees)",
required: false
]
def SetCoolingHigh = [
name: "SetCoolingHigh",
type: "number",
title: "Cooling Temperature (degrees)",
required: true
]
def pageName = "Ambiance"
def pageProperties = [
name: "TemperatureSettings",
title: "",
//nextPage: "ThermostatandDoors"
]
return dynamicPage(pageProperties) {
section("Select the main thermostat") {
input thermostat
}
section("Use remote sensors to adjust the thermostat (by default the app is using the internal sensor of the thermostat)") {
input "remoteSensors", "bool", title: "Enable remote sensor(s)", required: false, defaultValue: false, submitOnChange: true
if (remoteSensors) {
input sensor
paragraph "If multiple sensors are selected, the average temperature is automatically calculated"
}
}
section("When the temperature falls below this temperature (Low Temperature)..."){
input setLow
}
section("Adjust the thermostat to the following settings:"){
input cold
input SetHeatingLow
input SetCoolingLow
}
section("When the temperature raises above this temperature (High Temperature)..."){
input setHigh
}
section("Adjust the thermostat to the following settings:"){
input hot
input SetCoolingHigh
input SetHeatingHigh
}
section("When temperature is neutral (between above Low and High Temperatures..."){
input "neutral", "bool", title: "Turn off the thermostat", required: false, defaultValue: false
}
}
}
// Page - Disabled Mode
def ThermostatandDoors() {
def doors = [
name: "doors",
type: "capability.contactSensor",
title: "Which Sensor(s)?",
multiple: true,
required: false
]
def turnOffDelay = [
name: "turnOffDelay",
type: "decimal",
title: "Number of minutes",
required: false
]
def resetOff = [
name: "resetOff",
type: "bool",
title: "Reset Thermostat Settings when all Sensor(s) are closed",
required: false,
defaultValue: false
]
def pageName = "Thermostat and Doors"
def pageProperties = [
name: "ThermostatandDoors",
title: "",
//nextPage: "ThermostatAway"
]
return dynamicPage(pageProperties) {
section("When one or more contact sensors open, then turn off the thermostat") {
input doors
}
section("Wait this long before turning the thermostat off (defaults to 1 minute)") {
input turnOffDelay
input resetOff
}
}
}
// Page - Thermostat Away
def ThermostatAway() {
def modes2 = [
name: "modes2",
type: "mode",
title: "Which Location Mode(s)?",
multiple: true,
required: false
]
def away = [
name: "away",
type: "enum",
title: "Mode?",
metadata: [values:["auto", "heat", "cool", "off"]],
required: false
]
def setAwayLow = [
name: "setAwayLow",
type: "decimal",
title: "Low temp?",
required: false
]
def AwayCold = [
name: "AwayCold",
type: "enum",
title: "Mode?",
metadata: [values:["auto", "heat", "cool", "off"]],
required: false,
]
def setAwayHigh = [
name: "setAwayHigh",
type: "decimal",
title: "High temp?",
required: false
]
def AwayHot = [
name: "AwayHot",
type: "enum",
title: "Mode?",
required: false,
metadata: [values:["auto", "heat", "cool", "off"]]
]
def SetHeatingAway = [
name: "SetHeatingAway",
type: "number",
title: "Heating Temperature (degrees)",
required: false
]
def SetCoolingAway = [
name: "SetCoolingAway",
type: "number",
title: "Cooling Temperature (degrees)",
required: false
]
def fanAway = [
name: "fanAway",
type: "enum",
title: "Fan Mode?",
metadata: [values:["fanAuto", "fanOn", "fanCirculate"]],
required: false
]
def pageName = "Thermostat Away"
def pageProperties = [
name: "ThermostatAway",
title: "",
//nextPage: "Settings"
]
return dynamicPage(pageProperties) {
section("When the Location Mode changes to 'Away'") {
input modes2
}
section("Adjust the thermostat to the following settings:") {
input away
input fanAway
input SetHeatingAway
input SetCoolingAway
}
section("If the temperature falls below this temperature while away..."){
input setAwayLow
}
section("Automatically adjust the thermostat to the following operating mode..."){
input AwayCold
}
section("If the temperature raises above this temperature while away..."){
input setAwayHigh
}
section("Automatically adjust the thermostat to the following operating mode..."){
input AwayHot
}
}
}
// Show "Setup" page
def Settings() {
def days = [
name: "days",
type: "enum",
title: "Only on certain days of the week",
multiple: true,
required: false,
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
]
def modes = [
name: "modes",
type: "mode",
title: "Only when mode is",
multiple: true,
required: false
]
def pageName = ""
def pageProperties = [
name: "Settings",
title: "",
//nextPage: "pageSetup"
]
return dynamicPage(pageProperties) {
section("Notifications") {
input("recipients", "contact", title: "Send notifications to", multiple: true, required: false) {
paragraph "You may enter multiple phone numbers separated by semicolon."+
"E.G. 8045551122;8046663344"
input "sms", "phone", title: "To this phone", multiple: false, required: false
input "push", "bool", title: "Send Push Notification (optional)", required: false, defaultValue: false
}
}
section(title: "Restrictions", hideable: true) {
href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true
input days
input modes
}
section(title: "Debug") {
input "debug", "bool", title: "Enable debug messages in IDE for troubleshooting purposes", required: false, defaultValue: false, refreshAfterSelection:true
input "info", "bool", title: "Enable info messages in IDE to display actions in Live Logging", required: false, defaultValue: false, refreshAfterSelection:true
}
}
}
def installed(){
if (debug) log.debug "Installed called with $settings"
init()
}
def updated(){
if (debug) log.debug "Updated called with $settings"
unsubscribe()
init()
}
def init(){
state.lastStatus = null
runIn(60, "temperatureHandler")
if (debug) log.debug "Temperature will be evaluated in one minute"
if(sensor) {
subscribe(sensor, "temperature", temperatureHandler)
}
else {
subscribe(thermostat, "temperature", temperatureHandler)
}
if(modes2){
subscribe(location, modeAwayChange)
if(sensor) {
subscribe(sensor, "temperature", modeAwayTempHandler)
}
else {
subscribe(thermostat, "temperature", modeAwayTempHandler)
}
}
if(doors){
subscribe(doors, "contact.open", temperatureHandler)
subscribe(doors, "contact.closed", doorCheck)
state.disabledTemp = null
state.disabledMode = null
state.disableHSP = null
state.disableCSP = null
}
}
def temperatureHandler(evt) {
def currentTemp
if(modeOk && daysOk && timeOk && modeNotAwayOk) {
if(sensor){
//def sensors = sensor.size()
//def tempAVG = sensor ? getAverage(sensor, "temperature") : "undefined device"
//currentTemp = tempAVG
currentTemp = sensor.latestValue("temperature")
if (debug) log.debug "Data check (avg temp: ${currentTemp}, num of sensors:${sensors}, app status: ${lastStatus})"
}
else {
currentTemp = thermostat.latestValue("temperature")
if (debug) log.debug "Thermostat data (curr temp: ${currentTemp},status: ${lastStatus}"
}
if(setLow > setHigh){
def temp = setLow
setLow = setHigh
setHigh = temp
if(info) log.info "Detected ${setLow} > ${setHigh}. Auto-adjusting setting to ${temp}"
}
if (doorsOk) {
def currentMode = thermostat.latestValue("thermostatMode")
def currentHSP = thermostat.latestValue("heatingSetpoint")
def currentCSP = thermostat.latestValue("coolingSetpoint")
if (debug) log.debug "App data (curr temp: ${currentTemp},curr mode: ${currentMode}, currentHSP: ${currentHSP},"+
" currentCSP: ${currentCSP}, last status: ${lastStatus}"
if (currentTemp < setLow) {
if (state.lastStatus == "one" || state.lastStatus == "two" || state.lastStatus == "three" || state.lastStatus == null){
state.lastStatus = "one"
if (currentMode == "cool" || currentMode == "off") {
def msg = "Adjusting ${thermostat} operating mode and setpoints because temperature is below ${setLow}"
if (cold) thermostat?."${cold}"()
thermostat?.setHeatingSetpoint(SetHeatingLow)
if (SetCoolingLow) thermostat?.setCoolingSetpoint(SetCoolingLow)
thermostat?.poll()
sendMessage(msg)
if (info) log.info msg
}
else if (currentHSP < SetHeatingLow) {
def msg = "Adjusting ${thermostat} setpoints because temperature is below ${setLow}"
thermostat?.setHeatingSetpoint(SetHeatingLow)
if (SetCoolingLow) thermostat?.setCoolingSetpoint(SetCoolingLow)
thermostat?.poll()
sendMessage(msg)
if (info) log.info msg
}
}
}
if (currentTemp > setHigh) {
if (state.lastStatus == "one" || state.lastStatus == "two" || state.lastStatus == "three" || state.lastStatus == null){
state.lastStatus = "two"
if (currentMode == "heat" || currentMode == "off") {
def msg = "Adjusting ${thermostat} operating mode and setpoints because temperature is above ${setHigh}"
if (hot) thermostat?."${hot}"()
if (SetHeatingHigh) thermostat?.setHeatingSetpoint(SetHeatingHigh)
thermostat?.setCoolingSetpoint(SetCoolingHigh)
thermostat?.poll()
sendMessage(msg)
if (info) log.info msg
}
else if (currentCSP > SetCoolingHigh) {
def msg = "Adjusting ${thermostat} setpoints because temperature is above ${setHigh}"
thermostat?.setCoolingSetpoint(SetCoolingHigh)
if (SetHeatingHigh) thermostat?.setHeatingSetpoint(SetHeatingHigh)
thermostat?.poll()
sendMessage(msg)
if (info) log.info msg
}
}
}
if (currentTemp > setLow && currentTemp < setHigh) {
if (neutral == true) {
if (debug) log.debug "Neutral is ${neutral}, current temp is: ${currentTemp}"
if (state.lastStatus == "two" || state.lastStatus == "one" || state.lastStatus == null){
def msg = "Adjusting ${thermostat} mode to off because temperature is neutral"
thermostat?.off()
thermostat?.poll()
sendMessage(msg)
state.lastStatus = "three"
if (info) log.info msg
if (debug) log.debug "Data check neutral(neutral is:${neutral}, currTemp: ${currentTemp}, setLow: ${setLow}, setHigh: ${setHigh})"
}
}
if (info) log.info "Temperature is neutral not taking action because neutral mode is: ${neutral}"
}
}
else{
def delay = (turnOffDelay != null && turnOffDelay != "") ? turnOffDelay * 60 : 60
if(info) log.info ("Detected open doors. Checking door states again in ${delay} seconds")
runIn(delay, "doorCheck")
}
}
if (debug) log.debug "Temperature handler called: modeOk = $modeOk, daysOk = $daysOk, timeOk = $timeOk, modeNotAwayOk = $modeNotAwayOk "
}
def modeAwayChange(evt){
if(modeOk && daysOk && timeOk){
if (modes2){
if(modes2.contains(location.mode)){
state.lastStatus = "away"
if (away) thermostat."${away}"()
if(SetHeatingAway) thermostat.setHeatingSetpoint(SetHeatingAway)
if(SetCoolingAway) thermostat.setCoolingSetpoint(SetCoolingAway)
if(fanAway) thermostat.setThermostatFanMode(fanAway)
def msg = "Adjusting ${thermostat} mode and setpoints because Location Mode is set to Away"
sendMessage(msg)
if(info) log.info "Running AwayChange because mode is now ${away} and last staus is ${lastStatus}"
}
else {
state.lastStatus = null
temperatureHandler()
if(info) log.info "Running Temperature Handler because Home Mode is no longer in away, and the last staus is ${lastStatus}"
}
}
if(info) log.info ("Detected temperature change while away but all settings are ok, not taking any actions.")
}
}
def modeAwayTempHandler(evt) {
def tempAVGaway = sensor ? getAverage(sensor, "temperature") : "undefined device"
def currentAwayTemp = thermostat.latestValue("temperature")
if(info) log.info "Away: your average room temperature is: ${tempAVGaway}, current temp is ${currentAwayTemp}"
if (sensor) currentAwayTemp = tempAVGaway
if(lastStatus == "away"){
if(modes2.contains(location.mode)){
if (currentAwayTemp < setAwayLow) {
if(Awaycold) thermostat?."${Awaycold}"()
thermostat?.poll()
def msg = "I changed your ${thermostat} mode to ${Awaycold} because temperature is below ${setAwayLow}"
sendMessage(msg)
if (info) log.info msg
}
if (currentAwayTemp > setHigh) {
if(Awayhot) thermostat?."${Awayhot}"()
thermostat?.poll()
def msg = "I changed your ${thermostat} mode to ${Awayhot} because temperature is above ${setAwayHigh}"
sendMessage(msg)
if (info) log.info msg
}
}
else {
state.lastStatus = null
temperatureHandler()
if(info) log.info "Temp changed while staus is ${lastStatus} but the Location Mode is no longer in away. Resetting lastStatus"
}
}
}
def doorCheck(evt){
state.disabledTemp = sensor.latestValue("temperature")
state.disabledMode = thermostat.latestValue("thermostatMode")
state.disableHSP = thermostat.latestValue("heatingSetpoint")
state.disableCSP = thermostat.latestValue("coolingSetpoint")
if (debug) log.debug "Disable settings: ${state.disabledMode} mode, ${state.disableHSP} HSP, ${state.disableCSP} CSP"
if (!doorsOk){
if(info) log.info ("doors still open turning off ${thermostat}")
def msg = "I changed your ${thermostat} mode to off because some doors are open"
if (state.lastStatus != "off"){
thermostat?.off()
sendMessage(msg)
if (info) log.info msg
}
state.lastStatus = "off"
if (info) log.info "Changing status to off"
}
else {
if (state.lastStatus == "off"){
state.lastStatus = null
if (resetOff){
if(debug) log.debug "Contact sensor(s) are now closed restoring ${thermostat} with settings: ${state.disabledMode} mode"+
", ${state.disableHSP} HSP, ${state.disableCSP} CSP"
thermostat."${state.disabledMode}"()
thermostat.setHeatingSetpoint(state.disableHSP)
thermostat.setCoolingSetpoint(state.disableCSP)
}
}
temperatureHandler()
if(debug) "Calling Temperature Handler"
}
}
private getAverage(device,type){
def total = 0
if(debug) log.debug "calculating average temperature"
device.each {total += it.latestValue(type)}
return Math.round(total/device.size())
}
private void sendText(number, message) {
if (sms) {
def phones = sms.split("\\;")
for (phone in phones) {
sendSms(phone, message)
}
}
}
private void sendMessage(message) {
if(info) log.info "sending notification: ${message}"
if (recipients) {
sendNotificationToContacts(message, recipients)
if(debug) log.debug "sending notification: ${recipients}"
}
if (push) {
sendPush message
if(info) log.info "sending push notification"
} else {
sendNotificationEvent(message)
if(info) log.info "sending notification"
}
if (sms) {
sendText(sms, message)
if(debug) "Calling process to send text"
}
}
private getAllOk() {
modeOk && daysOk && timeOk && doorsOk && modeNotAwayOk
}
private getModeOk() {
def result = !modes || modes.contains(location.mode)
if(debug) log.debug "modeOk = $result"
result
}
private getModeNotAwayOk() {
def result = !modes2 || !modes2.contains(location.mode)
if(debug) log.debug "modeNotAwayOk = $result"
result
}
private getDoorsOk() {
def result = !doors || !doors.latestValue("contact").contains("open")
if(debug) log.debug "doorsOk = $result"
result
}
private getDaysOk() {
def result = true
if (days) {
def df = new java.text.SimpleDateFormat("EEEE")
if (location.timeZone) {
df.setTimeZone(location.timeZone)
}
else {
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
}
def day = df.format(new Date())
result = days.contains(day)
}
if(debug) log.debug "daysOk = $result"
result
}
private getTimeOk() {
def result = true
if (starting && ending) {
def currTime = now()
def start = timeToday(starting).time
def stop = timeToday(ending).time
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
}
else if (starting){
result = currTime >= start
}
else if (ending){
result = currTime <= stop
}
if(debug) log.debug "timeOk = $result"
result
}
def getTimeLabel(starting, ending){
def timeLabel = "Tap to set"
if(starting && ending){
timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending)
}
else if (starting) {
timeLabel = "Start at" + " " + hhmm(starting)
}
else if(ending){
timeLabel = "End at" + hhmm(ending)
}
timeLabel
}
private hhmm(time, fmt = "h:mm a")
{
def t = timeToday(time, location.timeZone)
def f = new java.text.SimpleDateFormat(fmt)
f.setTimeZone(location.timeZone ?: timeZone(time))
f.format(t)
}
def greyedOut(){
def result = ""
if (sensor) {
result = "complete"
}
result
}
def greyedOutTherm(){
def result = ""
if (thermostat) {
result = "complete"
}
result
}
def greyedOutTherm2(){
def result = ""
if (modes2) {
result = "complete"
}
result
}
def greyedOutSettings(){
def result = ""
if (starting || ending || days || modes || push) {
result = "complete"
}
result
}
def greyedOutTime(starting, ending){
def result = ""
if (starting || ending) {
result = "complete"
}
result
}
private anyoneIsHome() {
def result = false
if(people.findAll { it?.currentPresence == "present" }) {
result = true
}
if(debug) log.debug("anyoneIsHome: ${result}")
return result
}
page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) {
section {
input "starting", "time", title: "Starting (both are required)", required: false
input "ending", "time", title: "Ending (both are required)", required: false
}
}