Compare commits

..

11 Commits

Author SHA1 Message Date
Laurence Kahn
746b9a6a38 Modifying 'Smartapp to sync your simulated garage opener with physical devices.' 2015-10-14 14:19:48 -05:00
Laurence Kahn
98000fa57d MSA-618: Syncs a Simulated garage door device with 2 actual devices, either a tilt or contact sensor and a switch or relay. The simulated device will then control the actual garage door. In addition, the virtual device will sync when the garage door is opened manually, \n It also attempts to double check the door was actually closed in case the beam was crossed.
create a virtual device (see elsewhere for instructions) for device type use the simulated garage door.
You can also create a modified version for yourself I recommend decreasing the runin time to 1 sec, otherwise it takes 6 secs before the door starts closing or opening when you toggle the simulated device icon. (That is what I did)

install this smart app.

select your relay /switch, and actual garage door sensor in the app, Also select the virtual one twice in the app. Once for the sensor (to get the current state), and once for the door control to actually be able to toggle it.

See picts below.

It also optionally can send text or notifications

It also double checks (in 15 seconds) after opening or closing and will resync for some reason if the door didn't really close ie beam crossed, or didn't really open for some reason,
2015-10-12 21:54:17 -05:00
Vinay Rao
587b3295ae Merge pull request #186 from SmartThingsCommunity/sensor_name_missing
[DVCSMP-1164] Sensor name not shown
2015-10-12 12:52:27 -07:00
Vinay Rao
9538df65e5 [DVCSMP-1164] Sensor name not shown
During acceleration events, the sensor name is not shown.
2015-10-12 10:52:53 -07:00
Juan Pablo Risso
6854665f68 Merge pull request #177 from juano2310/Zen_PR
Adds capabilities "Temperature Measurement" and "Relative Humidity Me…
2015-10-09 13:29:31 -04:00
Juan Pablo Risso
2534afbf81 Removed Humidity 2015-10-09 12:39:26 -04:00
Juan Pablo Risso
eb3d0c2874 Adds capabilities "Temperature Measurement" and "Relative Humidity Measurement" 2015-10-09 12:34:58 -04:00
SmartThings, Inc.
5f85cd2873 Merge pull request #172 from SmartThingsCommunity/MSA-603-7
Merged publication request 'Keen Vent'
2015-10-05 16:28:52 -05:00
Donald C. Kirker
7bb6f67dbc MSA-603: Submission of Keen code to new SmartThings repo. 2015-10-05 16:26:47 -05:00
Kristofer Schaller
05cf0a0cb1 Merge pull request #170 from davidsulpy/master
Fixed a potential bug if a InitialState SmartApp hasn't been updated
2015-10-05 11:39:31 -07:00
David Sulpy
f012419710 added an initialization of the atomicState.eventBuffer if the eventBuffer is null when handling an event 2015-10-04 20:19:11 -05:00
6 changed files with 742 additions and 221 deletions

View File

@@ -0,0 +1,506 @@
/**
* Keen Home Smart Vent
*
* Author: Keen Home
* Date: 2015-06-23
*/
metadata {
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") {
capability "Switch Level"
capability "Switch"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Temperature Measurement"
capability "Battery"
command "getLevel"
command "getOnOff"
command "getPressure"
command "getBattery"
command "getTemperature"
command "setZigBeeIdTile"
fingerprint endpoint: "1",
profileId: "0104",
inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02",
outClusters: "0019"
}
// simulator metadata
simulator {
// status messages
status "on": "on/off: 1"
status "off": "on/off: 0"
// reply messages
reply "zcl on-off on": "on/off: 1"
reply "zcl on-off off": "on/off: 0"
}
// UI tile definitions
tiles {
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0"
state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff"
state "obstructed", action: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000"
}
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
state "level", action:"switch level.setLevel"
}
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("temperature", "device.temperature", inactiveLabel: false) {
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", inactiveLabel: false, decoration: "flat") {
state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff"
}
valueTile("zigbeeId", "device.zigbeeId", inactiveLabel: true, decoration: "flat") {
state "serial", label:'${currentValue}', backgroundColor:"#ffffff"
}
main "switch"
details(["switch","refresh","temperature","levelSliderControl","battery"])
}
}
/**** PARSE METHODS ****/
def parse(String description) {
log.debug "description: $description"
Map map = [:]
if (description?.startsWith('catchall:')) {
map = parseCatchAllMessage(description)
}
else if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
}
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('on/off: ')) {
map = parseOnOffMessage(description)
}
log.debug "Parse returned $map"
return map ? createEvent(map) : null
}
private Map parseCatchAllMessage(String description) {
log.debug "parseCatchAllMessage"
def cluster = zigbee.parse(description)
log.debug "cluster: ${cluster}"
if (shouldProcessMessage(cluster)) {
log.debug "processing message"
switch(cluster.clusterId) {
case 0x0001:
return makeBatteryResult(cluster.data.last())
break
case 0x0402:
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = convertTemperatureHex(temp)
return makeTemperatureResult(value)
break
case 0x0006:
return makeOnOffResult(cluster.data[-1])
break
}
}
return [:]
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
if (cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)) {
return false
}
return true
}
private Map parseReportAttributeMessage(String description) {
log.debug "parseReportAttributeMessage"
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
log.debug "Desc Map: $descMap"
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
return makeOnOffResult(Int.parseInt(descMap.value));
}
else if (descMap.cluster == "0008" && descMap.attrId == "0000") {
return makeLevelResult(descMap.value)
}
else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = convertTemperatureHex(descMap.value)
return makeTemperatureResult(value)
}
else if (descMap.cluster == "0001" && descMap.attrId == "0021") {
return makeBatteryResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == "0403" && descMap.attrId == "0020") {
return makePressureResult(Integer.parseInt(descMap.value, 16))
}
else if (descMap.cluster == "0000" && descMap.attrId == "0006") {
return makeSerialResult(new String(descMap.value.decodeHex()))
}
// shouldn't get here
return [:]
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
// log.debug "${description}"
// def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
// log.debug "split: " + description.split(": ")
def value = Double.parseDouble(description.split(": ")[1])
// log.debug "${value}"
resultMap = makeTemperatureResult(convertTemperature(value))
}
return resultMap
}
private Map parseOnOffMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('on/off: ')) {
def value = Integer.parseInt(description - "on/off: ")
resultMap = makeOnOffResult(value)
}
return resultMap
}
private Map makeOnOffResult(rawValue) {
log.debug "makeOnOffResult: ${rawValue}"
def linkText = getLinkText(device)
def value = rawValue == 1 ? "on" : "off"
return [
name: "switch",
value: value,
descriptionText: "${linkText} is ${value}"
]
}
private Map makeLevelResult(rawValue) {
def linkText = getLinkText(device)
// log.debug "rawValue: ${rawValue}"
def value = Integer.parseInt(rawValue, 16)
def rangeMax = 254
if (value == 255) {
log.debug "obstructed"
// Just return here. Once the vent is power cycled
// it will go back to the previous level before obstruction.
// Therefore, no need to update level on the display.
return [
name: "switch",
value: "obstructed",
descriptionText: "${linkText} is obstructed. Please power cycle."
]
} else if ( device.currentValue("switch") == "obstructed" &&
value == 254) {
// When the device is reset after an obstruction, the switch
// state will be obstructed and the value coming from the device
// will be 254. Since we're not using heating/cooling mode from
// the device type handler, we need to bump it down to the lower
// (cooling) range
sendEvent(makeOnOffResult(1)) // clear the obstructed switch state
value = rangeMax
}
// else if (device.currentValue("switch") == "off") {
// sendEvent(makeOnOffResult(1)) // turn back on if in off state
// }
// log.debug "pre-value: ${value}"
value = Math.floor(value / rangeMax * 100)
// log.debug "post-value: ${value}"
return [
name: "level",
value: value,
descriptionText: "${linkText} level is ${value}%"
]
}
private Map makePressureResult(rawValue) {
log.debug 'makePressureResut'
def linkText = getLinkText(device)
def pascals = rawValue / 10
def result = [
name: 'pressure',
descriptionText: "${linkText} pressure is ${pascals}Pa",
value: pascals
]
return result
}
private Map makeBatteryResult(rawValue) {
// log.debug 'makeBatteryResult'
def linkText = getLinkText(device)
// log.debug
[
name: 'battery',
value: rawValue,
descriptionText: "${linkText} battery is at ${rawValue}%"
]
}
private Map makeTemperatureResult(value) {
// log.debug 'makeTemperatureResult'
def linkText = getLinkText(device)
// log.debug "tempOffset: ${tempOffset}"
if (tempOffset) {
def offset = tempOffset as int
// log.debug "offset: ${offset}"
def v = value as int
// log.debug "v: ${v}"
value = v + offset
// log.debug "value: ${value}"
}
return [
name: 'temperature',
value: "" + value,
descriptionText: "${linkText} is ${value}°${temperatureScale}",
]
}
/**** HELPER METHODS ****/
private def convertTemperatureHex(value) {
// log.debug "convertTemperatureHex(${value})"
def celsius = Integer.parseInt(value, 16).shortValue() / 100
// log.debug "celsius: ${celsius}"
return convertTemperature(celsius)
}
private def convertTemperature(celsius) {
// log.debug "convertTemperature()"
if(getTemperatureScale() == "C"){
return celsius
} else {
def fahrenheit = Math.round(celsiusToFahrenheit(celsius) * 100) /100
// log.debug "converted to F: ${fahrenheit}"
return fahrenheit
}
}
private def makeSerialResult(serial) {
log.debug "makeSerialResult: " + serial
def linkText = getLinkText(device)
sendEvent([
name: "serial",
value: serial,
descriptionText: "${linkText} has serial ${serial}" ])
return [
name: "serial",
value: serial,
descriptionText: "${linkText} has serial ${serial}" ]
}
/**** COMMAND METHODS ****/
// def mfgCode() {
// ["zcl mfg-code 0x115B", "delay 200"]
// }
def on() {
log.debug "on()"
sendEvent(makeOnOffResult(1))
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
}
def off() {
log.debug "off()"
sendEvent(makeOnOffResult(0))
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
}
// does this work?
def toggle() {
log.debug "toggle()"
"st cmd 0x${device.deviceNetworkId} 1 6 2 {}"
}
def setLevel(value) {
log.debug "setting level: ${value}"
def linkText = getLinkText(device)
sendEvent(name: "level", value: value)
if (value > 0) {
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
}
else {
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
}
def rangeMax = 254
def computedLevel = Math.round(value * rangeMax / 100)
log.debug "computedLevel: ${computedLevel}"
def level = new BigInteger(computedLevel.toString()).toString(16)
log.debug "level: ${level}"
if (level.size() < 2){
level = '0' + level
}
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}"
}
def getOnOff() {
log.debug "getOnOff()"
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
}
def getPressure() {
log.debug "getPressure()"
[
"zcl mfg-code 0x115B", "delay 200",
"zcl global read 0x0403 0x20", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
]
}
def getLevel() {
log.debug "getLevel()"
// rattr = read attribute
// 0x${} = device net id
// 1 = endpoint
// 8 = cluster id (level control, in this case)
// 0 = attribute within cluster
// sendEvent(name: "level", value: value)
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
}
def getTemperature() {
log.debug "getTemperature()"
["st rattr 0x${device.deviceNetworkId} 1 0x0402 0"]
}
def getBattery() {
log.debug "getBattery()"
["st rattr 0x${device.deviceNetworkId} 1 0x0001 0x0021"]
}
def setZigBeeIdTile() {
log.debug "setZigBeeIdTile() - ${device.zigbeeId}"
def linkText = getLinkText(device)
sendEvent([
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
return [
name: "zigbeeId",
value: device.zigbeeId,
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
}
def refresh() {
getOnOff() +
getLevel() +
getTemperature() +
getPressure() +
getBattery()
}
private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
def configure() {
log.debug "CONFIGURE"
log.debug "zigbeeId: ${device.hub.zigbeeId}"
setZigBeeIdTile()
def configCmds = [
// binding commands
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
// configure report commands
// [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
// mike 2015/06/22: preconfigured; see tech spec
// vent on/off state - type: boolean, change: 1
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: preconfigured; see tech spec
// vent level - type: int8u, change: 1
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: temp and pressure reports are preconfigured, but
// we'd like to override their settings for our own purposes
// temperature - type: int16s, change: 0xA = 10 = 0.1C
"zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
// mike 2015/06/22: use new custom pressure attribute
// pressure - type: int32u, change: 1 = 0.1Pa
"zcl mfg-code 0x115B", "delay 200",
"zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 1500"
// mike 2015/06/22: preconfigured; see tech spec
// battery - type: int8u, change: 1
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
]
return configCmds + refresh()
}

View File

@@ -346,8 +346,8 @@ def getTemperature(value) {
log.debug "Acceleration"
def name = "acceleration"
def value = numValue.endsWith("1") ? "active" : "inactive"
//def linkText = getLinkText(device)
def descriptionText = "was $value"
def linkText = getLinkText(device)
def descriptionText = "$linkText was $value"
def isStateChange = isStateChange(device, name, value)
[
name: name,

View File

@@ -8,6 +8,7 @@ metadata {
definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") {
capability "Actuator"
capability "Thermostat"
capability "Temperature Measurement"
capability "Configuration"
capability "Refresh"
capability "Sensor"

View File

@@ -341,6 +341,13 @@ def eventHandler(name, value) {
def eventBuffer = atomicState.eventBuffer
def epoch = now() / 1000
// if for some reason this code block is being run
// but the SmartApp wasn't propery setup during install
// we need to set initialize the eventBuffer.
if (!atomicState.eventBuffer) {
atomicState.eventBuffer = []
}
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
log.debug eventBuffer

View File

@@ -0,0 +1,226 @@
/**
* 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.
*
* Author: LGKahn kahn-st@lgk.com
* version 2 user defineable timeout before checking if door opened or closed correctly. Raised default to 25 secs. can reduce to 15 if you have custom simulated door with < 6 sec wait.
*/
definition(
name: "LGK Virtual Garage Door",
namespace: "lgkapps",
author: "lgkahn kahn-st@lgk.com",
description: "Sync the Simulated garage door device with 2 actual devices, either a tilt or contact sensor and a switch or relay. The simulated device will then control the actual garage door. In addition, the virtual device will sync when the garage door is opened manually, \n It also attempts to double check the door was actually closed in case the beam was crossed. ",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/garage_contact@2x.png"
)
preferences {
section("Choose the switch/relay that opens closes the garage?"){
input "opener", "capability.switch", title: "Physical Garage Opener?", required: true
}
section("Choose the sensor that senses if the garage is open closed? "){
input "sensor", "capability.contactSensor", title: "Physical Garage Door Open/Closed?", required: true
}
section("Choose the Virtual Garage Door Device? "){
input "virtualgd", "capability.doorControl", title: "Virtual Garage Door?", required: true
}
section("Choose the Virtual Garage Door Device sensor (same as above device)?"){
input "virtualgdbutton", "capability.contactSensor", title: "Virtual Garage Door Open/Close Sensor?", required: true
}
section("Timeout before checking if the door opened or closed correctly?"){
input "checkTimeout", "number", title: "Door Operation Check Timeout?", required: true, defaultValue: 25
}
section( "Notifications" ) {
input("recipients", "contact", title: "Send notifications to") {
input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false
input "phone1", "phone", title: "Send a Text Message?", required: false
}
}
}
def installed()
{
def realgdstate = sensor.currentContact
def virtualgdstate = virtualgd.currentContact
//log.debug "in installed ... current state= $realgdstate"
//log.debug "gd state= $virtualgd.currentContact"
subscribe(sensor, "contact", contactHandler)
subscribe(virtualgdbutton, "contact", virtualgdcontactHandler)
// sync them up if need be set virtual same as actual
if (realgdstate != virtualgdstate)
{
if (realgdstate == "open")
{
virtualgd.open()
}
else virtualgd.close()
}
}
def updated()
{
def realgdstate = sensor.currentContact
def virtualgdstate = virtualgd.currentContact
//log.debug "in updated ... current state= $realgdstate"
//log.debug "in updated ... gd state= $virtualgd.currentContact"
unsubscribe()
subscribe(sensor, "contact", contactHandler)
subscribe(virtualgdbutton, "contact", virtualgdcontactHandler)
// sync them up if need be set virtual same as actual
if (realgdstate != virtualgdstate)
{
if (realgdstate == "open")
{
log.debug "opening virtual door"
mysend("Virtual Garage Door Opened!")
virtualgd.open()
}
else {
virtualgd.close()
log.debug "closing virtual door"
mysend("Virtual Garage Door Closed!")
}
}
// for debugging and testing uncomment temperatureHandlerTest()
}
def contactHandler(evt)
{
def virtualgdstate = virtualgd.currentContact
// how to determine which contact
//log.debug "in contact handler for actual door open/close event. event = $evt"
if("open" == evt.value)
{
// contact was opened, turn on a light maybe?
log.debug "Contact is in ${evt.value} state"
// reset virtual door if necessary
if (virtualgdstate != "open")
{
mysend("Garage Door Opened Manually syncing with Virtual Garage Door!")
virtualgd.open()
}
}
if("closed" == evt.value)
{
// contact was closed, turn off the light?
log.debug "Contact is in ${evt.value} state"
//reset virtual door
if (virtualgdstate != "closed")
{
mysend("Garage Door Closed Manually syncing with Virtual Garage Door!")
virtualgd.close()
}
}
}
def virtualgdcontactHandler(evt) {
// how to determine which contact
def realgdstate = sensor.currentContact
//log.debug "in virtual gd contact/button handler event = $evt"
//log.debug "in virtualgd contact handler check timeout = $checkTimeout"
if("open" == evt.value)
{
// contact was opened, turn on a light maybe?
log.debug "Contact is in ${evt.value} state"
// check to see if door is not in open state if so open
if (realgdstate != "open")
{
log.debug "opening real gd to correspond with button press"
mysend("Virtual Garage Door Opened syncing with Actual Garage Door!")
opener.on()
runIn(checkTimeout, checkIfActuallyOpened)
}
}
if("closed" == evt.value)
{
// contact was closed, turn off the light?
log.debug "Contact is in ${evt.value} state"
if (realgdstate != "closed")
{
log.debug "closing real gd to correspond with button press"
mysend("Virtual Garage Door Closed syncing with Actual Garage Door!")
opener.on()
runIn(checkTimeout, checkIfActuallyClosed)
}
}
}
private mysend(msg) {
if (location.contactBookEnabled) {
log.debug("sending notifications to: ${recipients?.size()}")
sendNotificationToContacts(msg, recipients)
}
else {
if (sendPushMessage != "No") {
log.debug("sending push message")
sendPush(msg)
}
if (phone1) {
log.debug("sending text message")
sendSms(phone1, msg)
}
}
log.debug msg
}
def checkIfActuallyClosed()
{
def realgdstate = sensor.currentContact
def virtualgdstate = virtualgd.currentContact
//log.debug "in checkifopen ... current state= $realgdstate"
//log.debug "in checkifopen ... gd state= $virtualgd.currentContact"
// sync them up if need be set virtual same as actual
if (realgdstate == "open" && virtualgdstate == "closed")
{
log.debug "opening virtual door as it didnt close.. beam probably crossed"
mysend("Resetting Virtual Garage Door to Open as real door didn't close (beam probably crossed)!")
virtualgd.open()
}
}
def checkIfActuallyOpened()
{
def realgdstate = sensor.currentContact
def virtualgdstate = virtualgd.currentContact
//log.debug "in checkifopen ... current state= $realgdstate"
//log.debug "in checkifopen ... gd state= $virtualgd.currentContact"
// sync them up if need be set virtual same as actual
if (realgdstate == "closed" && virtualgdstate == "open")
{
log.debug "opening virtual door as it didnt open.. track blocked?"
mysend("Resetting Virtual Garage Door to Closed as real door didn't open! (track blocked?)")
virtualgd.close()
}
}

View File

@@ -1,219 +0,0 @@
/**
* Pipe Freeze Preventer
*
* Copyright 2015 Simon Labrecque
*
* 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: "Pipe Freeze Preventer",
namespace: "simon-labrecque",
author: "Simon Labrecque",
description: "Pipe Freeze Preventer 'turns on' thermostats, by setting them to a configured heating setpoint, when it's been more than X minutes that they've been off. Used to make sure that hot water circulates in the pipes on a controlled schedule to prevent pipes freezing.",
category: "Safety & Security",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences {
section("About") {
paragraph textAbout()
}
section("Schedule settings")
{
input "everyXMinutes", "number", title: "Schedule to turn on at least every X minutes", defaultValue: "180"
input "leaveOnForXMinutes", "number", title: "Leave on for X minutes", defaultValue: "5"
input "minOutsideTemp", "text", title: "Outside temperature needs to be X or less for the schedule to run", defaultValue: "-10"
}
section("Thermostats settings"){
input "thermostats", "capability.thermostat", multiple: true, title: "Select Thermostats to monitor and control"
input "setTemperatureToThisToTurnOn", "number", title: "Heating setpoint used to 'turn on' the thermostat", defaultValue: "40"
}
section("Settings to use for Open Weather Map API (used to get outside temperature)"){
input "cityID", "text", title: "City ID", defaultValue: ""
input "apikey", "text", title: "API Key", defaultValue: ""
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize() {
subscribe(thermostats, "thermostatOperatingState", thermostatChange)
schedule("37 * * * * ?", "scheduleCheck")
state.currentlyUnfreezing = false
state.lastOnTime = now() - ((everyXMinutes) * 60 * 1000)
subscribe(app, onAppTouch)
}
def thermostatChange(evt) {
log.debug "thermostatChange: $evt.name: $evt.value"
if(evt.value == "heating") {
state.lastOnTime = now()
}
log.debug "state: " + state.lastOnTime
}
def scheduleCheck() {
log.debug "schedule check, lastOnTime = ${state.lastOnTime}, currentlyUnfreezing = ${state.currentlyUnfreezing}"
if(state.currentlyUnfreezing == false)
{
log.debug "scheduleCheck: calling checkIfNeedToUnfreeze"
checkIfNeedToUnfreeze()
}
else
{
log.debug "scheduleCheck: calling checkIfNeedToReturnToNormal"
checkIfNeedToReturnToNormal()
}
}
def checkIfNeedToReturnToNormal()
{
def unfreezingForInMinutes = ((now() - state.unfreezingSince) / 1000) / 60
log.debug "checkIfNeedToReturnToNormal: we've been unfreezing for " + unfreezingForInMinutes + " minutes"
if(unfreezingForInMinutes > leaveOnForXMinutes)
{
stopUnfreezing()
}
else
{
log.debug "checkIfNeedToReturnToNormal: continuing unfreezing"
}
}
def stopUnfreezing() {
log.debug "stopUnfreezing: setting back thermostats to their original heatingSetPoint"
for (int i = 0; i < thermostats.size(); i++) {
thermostats[i].setHeatingSetpoint(state.tstatHeatingSetpointBackup[i])
}
state.currentlyUnfreezing = false;
state.lastOnTime = now()
}
def checkIfNeedToUnfreeze()
{
def currentOutsideTemperature = getCurrentOutsideTemperature()
BigDecimal minTemperatureDecimal = new BigDecimal(minOutsideTemp)
log.debug "checkIfNeedToUnfreeze: current oustide temperature is " + currentOutsideTemperature + "C, min temperature to run is " + minTemperatureDecimal
if(currentOutsideTemperature > minTemperatureDecimal)
{
log.debug "checkIfNeedToUnfreeze: no need to unfreeze since outside temperature is more than " + minOutsideTemp
return
}
for (tstat in thermostats) {
if(tstat.currentTemperature < tstat.currentHeatingSetpoint)
{
//We suppose that the thermostatOperatingState is heating even tough it wasn't reported
log.debug "checkIfNeedToUnfreeze: assuming that thermostat '" + tstat.label + "' is on since temperature is " + tstat.currentTemperature + " and setpoint is " + tstat.currentHeatingSetpoint
state.lastOnTime = now()
}
}
def minutesSinceLastOnTime = ((now() - state.lastOnTime) / 1000) / 60
log.debug "checkIfNeedToUnfreeze: " + minutesSinceLastOnTime + " minutes since our last unfreeze"
if(minutesSinceLastOnTime < everyXMinutes)
{
log.debug "checkIfNeedToUnfreeze: not turning on because we haven't reached " + everyXMinutes + " minutes yet"
return
}
//It's been more than everyXMinutes, so turning on thermostats
startUnfreezing()
}
def startUnfreezing() {
log.debug "startUnfreezing: starting unfreezing for " + leaveOnForXMinutes + " minutes"
state.currentlyUnfreezing = true
state.unfreezingSince = now()
state.tstatHeatingSetpointBackup = []
for (int i = 0; i < thermostats.size(); i++) {
log.debug "startUnfreezing: setting new heatingSetPoint for tstat " + thermostats[i].label
state.tstatHeatingSetpointBackup.add(thermostats[i].currentHeatingSetpoint)
thermostats[i].setHeatingSetpoint(setTemperatureToThisToTurnOn)
}
log.debug "startUnfreezing: turned on thermostats by setting temp to " + setTemperatureToThisToTurnOn
log.debug "startUnfreezing: tstat heatpoint backup: " + state.tstatHeatingSetpointBackup
log.debug "startUnfreezing: state.currentlyUnfreezing="+state.currentlyUnfreezing
}
BigDecimal getCurrentOutsideTemperature() {
def params = [
uri: 'http://api.openweathermap.org/data/2.5/',
path: 'weather',
contentType: 'application/json',
query: [mode: 'json', units: 'metric', APPID: apikey, id: cityID]
]
def currentTemperature = 0G
try {
httpGet(params) {resp ->
log.debug "resp data: ${resp.data}"
currentTemperature = resp.data.main.temp
}
} catch (e) {
log.error "error: $e"
}
return currentTemperature
}
private def textAbout() {
return '''\
Pipe Freeze Preventer 'turns on' thermostats, by setting them to a configured heating setpoint, \
when it's been more than X minutes that they've been off. Used to make sure that hot water circulates \
in the pipes on a controlled schedule to prevent pipes freezing. \
'''
}
def onAppTouch(event) {
log.debug "onAppTouch: currentlyUnfreezing: ${state.currentlyUnfreezing} lastOnTime:${state.lastOnTime}"
if(state.currentlyUnfreezing == false)
{
log.debug "onAppTouch: calling startUnfreezing()"
startUnfreezing()
}
else
{
log.debug "onAppTouch: calling stopUnfreezing()"
stopUnfreezing()
}
}