Compare commits

..

1 Commits

Author SHA1 Message Date
Mike Sprycha
4d289dbd6b MSA-1339: Adding support for the Sercomm SZ-DWS04 is simply a matter or adding an additional fingerprint to already existing Smartsense Open/Close sensor.
Please consider adding it as either a separate device handler or as an additional fingerprint to existing handler.
2016-06-05 13:28:33 -05:00
4 changed files with 387 additions and 396 deletions

View File

@@ -0,0 +1,326 @@
/**
* Sercomm SZ-DWS04 Open/Closed Sensor
*
* 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: "Sercomm SZ-DWS04 Open/Closed Sensor", namespace: "mikespry", author: "Mike Sprycha") {
capability "Battery"
capability "Configuration"
capability "Contact Sensor"
capability "Refresh"
capability "Temperature Measurement"
command "enrollResponse"
fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "Sercomm Corp.", model: "SZ-DWS04N_SF", deviceJoinName: "Sercomm SZ-DWS04"
}
simulator {
}
preferences {
input title: "Temperature Offset", description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
}
tiles(scale: 2) {
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
}
}
valueTile("temperature", "device.temperature", inactiveLabel: false, 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:""
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
}
main (["contact", "temperature"])
details(["contact","temperature","battery","refresh"])
}
}
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: ')) {
map = parseCustomMessage(description)
}
else if (description?.startsWith('zone status')) {
map = parseIasMessage(description)
}
log.debug "Parse returned $map"
def result = map ? createEvent(map) : null
if (description?.startsWith('enroll request')) {
List cmds = enrollResponse()
log.debug "enroll response: ${cmds}"
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
}
return result
}
private Map parseCatchAllMessage(String description) {
Map resultMap = [:]
def cluster = zigbee.parse(description)
if (shouldProcessMessage(cluster)) {
switch(cluster.clusterId) {
case 0x0001:
resultMap = getBatteryResult(cluster.data.last())
break
case 0x0402:
log.debug 'TEMP'
// temp is last 2 data values. reverse to swap endian
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
def value = getTemperature(temp)
resultMap = getTemperatureResult(value)
break
}
}
return resultMap
}
private boolean shouldProcessMessage(cluster) {
// 0x0B is default response indicating message got through
// 0x07 is bind message
boolean ignoredMessage = cluster.profileId != 0x0104 ||
cluster.command == 0x0B ||
cluster.command == 0x07 ||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
return !ignoredMessage
}
private int getHumidity(value) {
return Math.round(Double.parseDouble(value))
}
private Map parseReportAttributeMessage(String description) {
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"
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
resultMap = getTemperatureResult(value)
}
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
}
private Map parseCustomMessage(String description) {
Map resultMap = [:]
if (description?.startsWith('temperature: ')) {
def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
resultMap = getTemperatureResult(value)
}
return resultMap
}
private Map parseIasMessage(String description) {
List parsedMsg = description.split(' ')
String msgCode = parsedMsg[2]
Map resultMap = [:]
switch(msgCode) {
case '0x0020': // Closed/No Motion/Dry
resultMap = getContactResult('closed')
break
case '0x0021': // Open/Motion/Wet
resultMap = getContactResult('open')
break
case '0x0022': // Tamper Alarm
break
case '0x0023': // Battery Alarm
break
case '0x0024': // Supervision Report
resultMap = getContactResult('closed')
break
case '0x0025': // Restore Report
resultMap = getContactResult('open')
break
case '0x0026': // Trouble/Failure
break
case '0x0028': // Test Mode
break
}
return resultMap
}
def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){
return celsius
} else {
return celsiusToFahrenheit(celsius) as Integer
}
}
private Map getBatteryResult(rawValue) {
log.debug 'Battery'
def linkText = getLinkText(device)
def result = [
name: 'battery'
]
def volts = rawValue / 10
def descriptionText
if (rawValue == 0 || rawValue == 255) {}
else if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
return result
}
private Map getTemperatureResult(value) {
log.debug 'TEMP'
def linkText = getLinkText(device)
if (tempOffset) {
def offset = tempOffset as int
def v = value as int
value = v + offset
}
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
return [
name: 'temperature',
value: value,
descriptionText: descriptionText
]
}
private Map getContactResult(value) {
log.debug 'Contact Status'
def linkText = getLinkText(device)
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
return [
name: 'contact',
value: value,
descriptionText: descriptionText
]
}
def refresh() {
log.debug "Refreshing Temperature and Battery"
def refreshCmds = [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
]
return refreshCmds + enrollResponse()
}
def configure() {
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting, IAS CIE, and Bindings."
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
]
return configCmds + refresh() // send refresh cmds as part of config
}
def enrollResponse() {
log.debug "Sending enroll response"
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
[
//Resending the CIE in case the enroll request is sent before CIE is written
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
//Enroll Response
"raw 0x500 {01 23 00 00 00}",
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
]
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}
private hex(value) {
new BigInteger(Math.round(value).toString()).toString(16)
}
private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}
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
}

View File

@@ -403,21 +403,39 @@ def refresh() {
if (device.getDataValue("manufacturer") == "SmartThings") {
log.debug "Refreshing Values for manufacturer: SmartThings "
/* These values of Motion Threshold Multiplier(0x01) and Motion Threshold (0x0276)
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
Separating these out in a separate if-else because I do not want to touch Centralite part
as of now.
*/
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode])
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode])
refreshCmds = refreshCmds + [
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
Separating these out in a separate if-else because I do not want to touch Centralite part
as of now.
*/
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
} else {
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode])
refreshCmds = refreshCmds + [
/* sensitivity - default value (8) */
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
]
}
//Common refresh commands
refreshCmds += zigbee.readAttribute(0x0402, 0x0000) +
zigbee.readAttribute(0x0001, 0x0020) +
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode])
refreshCmds = refreshCmds + [
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global read 0xFC02 0x0010",
"send 0x${device.deviceNetworkId} 1 1","delay 400"
]
return refreshCmds + enrollResponse()
}
@@ -425,15 +443,38 @@ def refresh() {
def configure() {
sendEvent(name: "checkInterval", value: 7200, displayed: false)
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
log.debug "Configuring Reporting"
def configCmds = enrollResponse() +
zigbee.batteryConfig() +
zigbee.temperatureConfig() +
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
def configCmds = [
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", "delay 200", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {0100}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {0100}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zcl mfg-code ${manufacturerCode}", "delay 200",
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {0100}", "delay 200",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh()
}

View File

@@ -21,13 +21,6 @@ metadata {
capability "Motion Sensor"
capability "Sensor"
capability "Battery"
fingerprint mfr: "011F", prod: "0001", model: "0001", deviceJoinName: "Schlage Motion Sensor" // Schlage motion
fingerprint mfr: "014A", prod: "0001", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion
fingerprint mfr: "014A", prod: "0004", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion +
fingerprint mfr: "0060", prod: "0001", model: "0002", deviceJoinName: "Everspring Motion Sensor" // Everspring SP814
fingerprint mfr: "0060", prod: "0001", model: "0003", deviceJoinName: "Everspring Motion Sensor" // Everspring HSP02
fingerprint mfr: "011A", prod: "0601", model: "0901", deviceJoinName: "Enerwave Motion Sensor" // Enerwave ZWN-BPC
}
simulator {
@@ -132,9 +125,9 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
}
if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) {
result << response(zwave.batteryV1.batteryGet())
} else {
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
result << response("delay 1200")
}
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
result
}

View File

@@ -1,369 +0,0 @@
/**
* Programmable Thermostat
*
* Copyright 2016 Raymond Ciarcia
*
* 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: "Programmable Thermostat",
namespace: "smartthings",
author: "Raymond Ciarcia",
description: "A full-featured, easy to use interface for programming your thermostat based on schedule setpoints and mode changes",
category: "Convenience",
iconUrl: "http://cdn.device-icons.smartthings.com/Home/home1-icn@2x.png",
iconX2Url: "http://cdn.device-icons.smartthings.com/Home/home1-icn@2x.png",
iconX3Url: "http://cdn.device-icons.smartthings.com/Home/home1-icn@2x.png"
)
preferences {
page(name:"Settings", title:"Settings", uninstall:true, install:true ) {
section() {
input (name:"thermostat", type: "capability.thermostat", title: "Select thermostat", required: true, multiple: false)
}
section ("Scheduled Setpoints") {
input (name: "son", type: "bool", title: "Run scheduled setpoints", required:true)
input (name:"numscheduled", title: "Number of scheduled setpoints", type: "number",refreshAfterSelection: true)
href(title: "Schedule Setpoints", page: "ScheduledChanges")
}
section ("Mode-Based Setpoints") {
input (name: "eon", type: "bool", title: "Run mode-based setpoints", required:true)
input (name:"numevent", title: "Number of mode-based setpoints", type: "number",refreshAfterSelection: true)
href(title: "Mode-Based Setpoints Setpoints", page: "EventBasedChanges")
}
section("Auto Thermostat Mode Control") {
input (name: "auto", type: "enum", title: "Adjust thermostat heating/cooling mode based on current temperature and setpoint", required:true, multiple: false, options: ['Never','When setpoints are executed','Any time'])
}
section("Notifications") {
input (name: "snotifications", type: "bool", title: "Notify when scheduled setpoints execute", required:true)
input (name: "enotifications", type: "bool", title: "Notify when mode-based setpoints execute", required:true)
input (name: "eventlogging", type: "enum", title: "Set the level of event logging in the notification feed", required:true, multiple: false, options: ['None','Normal','Detailed'])
}
section("Command Acknowledgement Failure Response and Notification") {
input (name: "fnotifications", type: "bool", title: "Resend commands not acknowledged by the theromstat and notify after multiple failed attempts. Increases thermostat reliability but may not be compatible with all thermostats; disable if every command results in a failure notification.", required:true)
}
}
page(name: "ScheduledChanges")
page(name: "EventBasedChanges")
}
def ScheduledChanges() {
dynamicPage(name: "ScheduledChanges", uninstall: true, install: false) {
for (int i = 1; i <= settings.numscheduled; i++) {
section("Scheduled Setpoint $i") {
input "stime${i}", "time", title: "At this time:", required: true
input "sheatset${i}", "decimal", title: "Set this heating temperature:", required: true
input "scoolset${i}", "decimal", title: "Set this cooling temperature:", required: true
input "sdays${i}", "enum", title: "Only on these days (no selection is equivalent to selecting all):", required: false, multiple: true, options: ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
input "smodes${i}", "mode", title: "Only in these modes (no selection is equivalent to selecting all):", multiple: true, required: false
}
}
}
}
def EventBasedChanges() {
dynamicPage(name: "EventBasedChanges", uninstall: true, install: false) {
for (int i = 1; i <= settings.numevent; i++) {
section("Mode-Based Setpoint $i") {
input "emodes${i}", "mode", title: "On transition to this mode:", multiple: false, required: true
input "eheatset${i}", "decimal", title: "Set this heating temperature:", required: true
input "ecoolset${i}", "decimal", title: "Set this cooling temperature:", required: true
input "edays${i}", "enum", title: "Only on these days (no selection is equivalent to selecting all):", required: false, multiple: true, options: ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
}
}
}
}
//---- INSTALL AND UPDATE
def installed() {initialize()}
def updated() {initialize()}
def initialize() {
try {
unschedule()
} catch(e) {
try {
unschedule(SchedulerIntegrityChecker)
unschedule(MidnightRunner)
} catch(ev) {}
}
unsubscribe()
subscribe(settings.thermostat, "temperature", tempChangeHandler)
if ((settings.numevent > 0) && (settings.eon)) {subscribe(location, modeChangeHandler)}
state.scheduledindex = 0
state.pendingindex = 0
state.dayoflastrun = TodayAsString()
state.timeoflastevent = now()
state.nextscheduledtime = now()
state.failedcommandcount = 0
state.schedulestring = ""
state.checkcommandstring = ""
state.eventlogging = 0
if (settings.eventlogging == "Normal"){state.notificationlevel = 1}
if (settings.eventlogging == "Detailed"){state.notificationlevel = 2}
if ((settings.numscheduled > 0) && (settings.son)) {
schedule(timeToday("2015-08-04T00:00:00.000",location.timeZone), MidnightRunner)
SchedulerFunction()
}
log.debug "Programmable Thermostat: successfully initialized."
if (state.notificationlevel>0) {sendNotificationEvent("Programmable Thermostat successfully initialized.$state.schedulestring.")}
state.schedulestring = ""
}
//---- SCHEDULING FUNCTIONS
//At midnight, runs scheduler function to set the first scheduled event of the new day
def MidnightRunner() {
state.dayoflastrun = TodayAsString()
state.timeoflastevent = now()
SchedulerFunction()
def i = SearchSchedulePoints("2015-08-04T00:00:00.000")
if (i>0) {ThermostatCommander(settings."sheatset${i}", settings."scoolset${i}", settings.snotifications, "per scheduled setpoint.$state.schedulestring")}
}
//Determines and schedules the next scheduled setpoint
def SchedulerFunction(){
def mindiff = 60*60*1000*24*7
def timeNow = now()
def todaystring = TodayAsString()
for (int i = 1; i <= settings.numscheduled; i++) {
def ScheduledTime = timeToday(settings["stime$i"],location.timeZone)
def ScheduledDays = settings["sdays$i"]
if (ScheduledDays == null) {ScheduledDays = TodayAsString()}
if (ScheduledTime != null) {
if ((ScheduledTime.time >= timeNow) && (ScheduledDays.contains(TodayAsString())) && (ScheduledTime.time - timeNow < mindiff)){
mindiff = ScheduledTime.time - timeNow
state.scheduledindex = i
}
}
}
if (mindiff < 60*60*1000*24*7) {
int i = state.scheduledindex
def nextrun = timeToday(settings["stime$i"],location.timeZone)
state.nextscheduledtime = nextrun.time
def nextrunstring = DisplayTime(nextrun)
runOnce(nextrun, ScheduleExecuter)
if (state.notificationlevel>1) {state.schedulestring=" Next scheduled setpoint for $thermostat.label is today at $nextrunstring"}
} else {
state.nextscheduledtime = -1
if (state.notificationlevel>1) {state.schedulestring=" There are no remaining scheduled setpoints for $thermostat.label today"}
}
state.timeoflastevent = now()
}
def SearchSchedulePoints(time) {
for (int i = 1; i <= settings.numscheduled; i++) {
def Modes = settings["smodes$i"]
if (Modes == null) {Modes = location.mode}
def Days = settings["sdays$i"]
if (Days == null) {Days = TodayAsString()}
if(timeToday(settings["stime$i"],location.timeZone) == timeToday(time,location.timeZone) && Modes.contains(location.mode) && Days.contains(TodayAsString())) {
return i
}
}
return 0
}
//---- EXECUTION FUNCTIONS
//Runs at scheduled setpoints to determine whether a setpoint should be executed; if yes, calls thermostat commander to execute command
def ScheduleExecuter() {
int i = state.scheduledindex
SchedulerFunction()
state.timeoflastevent = now()
def valid = false
def Modes = settings["smodes$i"]
if (Modes == null) {Modes = location.mode}
if(Modes.contains(location.mode)){
valid = true
} else {
i = SearchSchedulePoints(settings["stime$i"])
if (i > 0) {valid = true}
}
if (valid) {
state.failedcommandcount = 0
state.pendingindex = i
ThermostatCommander(settings."sheatset${i}", settings."scoolset${i}", settings.snotifications, "per scheduled setpoint.$state.schedulestring")
} else {
if (state.notificationlevel>1) {sendNotificationEvent("Scheduled setpoint for $thermostat.label not executed because the current home mode, $location.mode, does not match a setpoint mode.$state.schedulestring.")}
}
state.schedulestring = ""
}
//Sends commands to the thermostat
def ThermostatCommander(hvalue, cvalue, notifications, notificationphrase) {
state.timeoflastevent = now()
if((hvalue == null) || (cvalue == null)) {return}
if (settings.auto != "Never") {ThermostatModeSetter(hvalue, cvalue, 0)}
def notificationstring = ""
state.checkcommandstring = ""
def thermMode = thermostat.currentValue("thermostatMode")
def name = thermostat.label
def currentheatsetpoint = settings.thermostat.currentValue("heatingSetpoint")
def currentcoolsetpoint = settings.thermostat.currentValue("coolingSetpoint")
if ("$currentcoolsetpoint" != "$cvalue") {state.checkcommandstring = "c"}
if ("$currentheatsetpoint" != "$hvalue") {state.checkcommandstring = "h$state.checkcommandstring"}
log.debug "Programmable Thermostat: check string is $state.checkcommandstring; values are $currentcoolsetpoint and $currentheatsetpoint"
def primarysetpoint = hvalue
if (thermMode == "cool") {primarysetpoint = cvalue}
if (thermMode == "heat" || thermMode == "cool") {notificationstring = "$name set to $primarysetpoint in $thermMode mode $notificationphrase."}
else {notificationstring = "$name set to $hvalue / $cvalue $notificationphrase."}
if (settings.fnotifications && state.checkcommandstring != "") (runIn(10 + state.failedcommandcount*30, CommandIntegrityChecker))
if (hvalue!=0) {
log.debug "Programmable Thermostat: Heat command set to $hvalue"
thermostat.setHeatingSetpoint(hvalue)
}
if (cvalue!=0) {
log.debug "Programmable Thermostat: Cool command set to $cvalue"
thermostat.setCoolingSetpoint(cvalue)
}
if (notifications && state.failedcommandcount==0) {
sendPush(notificationstring)
} else if (state.notificationlevel>0 && state.failedcommandcount==0) {
sendNotificationEvent(notificationstring)
if (state.checkcommandstring == "" && state.notificationlevel>2) {sendNotificationEvent("$name confirmed that it was already set to $primarysetpoint in $thermMode mode.")}
}
if (state.checkcommandstring == "") {log.debug "Programmable Thermostat: $name confirmed that it was already set to $primarysetpoint in $thermMode mode"}
}
//Auto Sets Thermostat Mode
def ThermostatModeSetter(hvalue, cvalue, notifications) {
if (hvalue==0 || cvalue==0) {return}
def currentTemp = settings.thermostat.latestValue("temperature")
if (currentTemp > cvalue && settings.thermostat.currentValue("thermostatMode") != "cool") {
thermostat.cool()
if (notifications > 0) {sendNotificationEvent("$thermostat.label mode changed to cooling when temperature reached $currentTemp")}
} else if (currentTemp < hvalue && settings.thermostat.currentValue("thermostatMode") != "heat") {
thermostat.heat()
if (notifications > 0) {sendNotificationEvent("$thermostat.label mode changed to heating when temperature fell to $currentTemp")}
}
}
//---- INTEGRITY CHECKERS
//Determines whether the last scheduled setpoint was executed; if not, reinitializes or sends missed command
def SchedulerIntegrityChecker() {
def i = state.scheduledindex
if ((settings.numscheduled == 0) || (settings.son == false)) {return}
if (state.dayoflastrun != TodayAsString()) {initialize()}
}
//Determines whether commands sent to the thermostat have been properly acknowledged; if not, calls thermostat commander to reissue failed command(s)
def CommandIntegrityChecker() {
state.timeoflastevent = now()
if (state.pendingindex == 0) {return}
def currentheatsetpoint = settings.thermostat.currentValue("heatingSetpoint")
def currentcoolsetpoint = settings.thermostat.currentValue("coolingSetpoint")
def thermMode = thermostat.currentValue("thermostatMode")
def lastheatcommand = IndexLookUp("heat")
def lastcoolcommand = IndexLookUp("cool")
def failedstring = ""
log.debug "Programmable Thermostat: $thermostat.label heating setpoint was commanded to $lastheatcommand and is currently $currentheatsetpoint; cooling setpoint was commanded to $lastcoolcommand and is currently $currentcoolsetpoint"
if (("$currentheatsetpoint" == "$lastheatcommand") && ("$currentcoolsetpoint" == "$lastcoolcommand")) {return}
state.failedcommandcount = state.failedcommandcount + 1
if ("$currentheatsetpoint" != "$lastheatcommand" && "$currentcoolsetpoint" != "$lastcoolcommand" && state.checkcommandstring == "hc") {
failedstring = "$thermostat.label is non-responsive to setpoint commands."
ThermostatCommander(lastheatcommand, lastcoolcommand, false, "")
} else if ("$currentheatsetpoint" != "$lastheatcommand" && (state.checkcommandstring == "hc" || state.checkcommandstring == "h")) {
if (thermMode == "heat") {failedstring = "$thermostat.label is non-responsive to heat setpoint commands."}
ThermostatCommander(lastheatcommand, 0, false, "")
} else if ("$currentcoolsetpoint" != "$lastcoolcommand" && (state.checkcommandstring == "hc" || state.checkcommandstring == "c")) {
if (thermMode == "cool") failedstring = "$thermostat.label is non-responsive to cool setpoint commands."
ThermostatCommander(0, lastcoolcommand, false, "")
}
if (state.failedcommandcount == 4) {
state.failedcommandcount = 0
state.pendingindex = 0
if (failedstring != "") {sendPush(failedstring)}
}
}
//---- EVENT HANDLERS
//Runs every time a mode change is detected. Used to execute mode-based setpoints; also used to trigger schedule integrity checks in case all scheduled functions have failed
def modeChangeHandler(evt) {
if (state.notificationlevel>2) {sendNotificationEvent("Programmable Thermostat detected home mode change to $evt.value.")}
for (int i = 1; i <= settings.numevent; i++) {
def ScheduledDays = settings["edays$i"]
if (ScheduledDays == null) {ScheduledDays = TodayAsString()}
if ((evt.value == settings["emodes$i"]) && (ScheduledDays.contains(TodayAsString()))) {
state.failedcommandcount = 0
state.pendingindex = -i
ThermostatCommander(settings."eheatset${i}", settings."ecoolset${i}", settings.enotifications, "with change to $evt.value")
i = settings.numevent + 1
}
}
SchedulerIntegrityChecker()
}
//Runs every time the temperature reported by the thermostat changes. Used to trigger schedule integrity checks in case all scheduled functions have failed.
def tempChangeHandler(evt) {
SchedulerIntegrityChecker()
if (settings.auto == "Any time") {ThermostatModeSetter(settings.thermostat.latestValue("heatingSetpoint"), settings.thermostat.latestValue("coolingSetpoint"), state.notificationlevel)}
}
//---- OTHER
//Returns the setpoint temperature associated with a settings index
def IndexLookUp(mode) {
def result = 0
if (mode == "cool") {
if (state.pendingindex > 0) {result = settings."scoolset${state.pendingindex}"}
if (state.pendingindex < 0) {result = settings."ecoolset${-state.pendingindex}"}
} else if (mode == "heat") {
if (state.pendingindex > 0) {result = settings."sheatset${state.pendingindex}"}
if (state.pendingindex < 0) {result = settings."eheatset${-state.pendingindex}"}
}
return result
}
//Returns the current day of the week as a string
def TodayAsString() {
return (new Date(now())).format("EEEEEEE", location.timeZone)
}
//Returns time as a string in 12 hour format
def DisplayTime(time) {
def tz = location.timeZone
def hour = time.format("H",tz)
def min = time.format("m",tz)
def sec = time.format("s",tz)
def ampm = "am"
def hournum = hour.toInteger()
def minnum = min.toInteger()
if (hournum == 0) {hournum = 12}
if (hournum > 12) {
hournum = hournum - 12
ampm = "pm"
}
if (minnum < 10) {min = "0$min"}
return "$hournum:$min $ampm"
}