Dashboard feature request - thermostat template

With winter closing in fast here in New England, any chance on getting a thermostat tile soon? For reference, I'm using the built in Nest integration. :grin:
@patrick

thanks

2 Likes

I also throw my hat in the ring for a Thermostat tile please!

+1

2 Likes

How do you have your blinds setup? On/off or dimmer?

I am using a ZWave Shade driver that I found on the ST forum. I set it up so long ago that I forgot I wasn't using a built in driver until I looked this up right now.

In the Dashboard, I am using a Shade template

> 
> /**
>  *  Copyright 2016 ericvitale@gmail.com
>  *
>  *  Version 1.0.6 - Cleaned up a bit. 06/30/2017
>  *  Version 1.0.5 - Added auto-detect support for Somfy by Bali. 03/31/2017
>  *  Version 1.0.4 - Added support for the Window Shade capability. 10/15/2016
>  *  Version 1.0.3 - Tweaked configuration calling.
>  *  Version 1.0.2 - Added support for poll, fixed battery reporting bug.
>  *  Version 1.0.1 - Added support for battery level.
>  *  Version 1.0.0 - Initial Release
>  *
>  *  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.
>  *
>  *  You can find this device handler @ https://github.com/ericvitale/ST-Z-Wave-Shade
>  *  You can find my other device handlers & SmartApps @ https://github.com/ericvitale
>  *
>  *  Credit to SmartThings for the following device handler
>  *  https://github.com/SmartThingsCommunity/SmartThingsPublic/blob/master/devicetypes/smartthings/zwave-dimmer-switch-generic.src/zwave-dimmer-switch-generic.groovy
>  *
>  */
>  
> def dhVersion() { return "1.0.2" } 
>  
> metadata {
> 	definition (name: "Z-Wave Shade", namespace: "ericvitale", author: "ericvitale@gmail.com") {
> 		capability "Switch Level"
> 		capability "Actuator"
> 		capability "Switch"
> 		capability "Polling"
> 		capability "Refresh"
> 		capability "Sensor"
>         capability "Battery"
>         capability "Configuration"
>         capability "Window Shade"
>         
>         command "sceneOne"
>         command "sceneTwo"
>         command "sceneThree"
>         command "sceneFour"
>         command "sceneFive"
>         command "getBattery"
>         command "doPoll"
>         command "levelOpenClose"
>         
>         attribute "lastActivity", "string"
>         attribute "lastConfigured", "string"
>         attribute "lastPoll", "string"
>         attribute "lastBattery", "string"
>         
>         fingerprint mfr: "026E", prod: "4345", model: "0038"
>         fingerprint mfr: "026E", prod: "4345", model: "5A31"
>         fingerprint deviceId: "0x1007", inClusters: "0x5E,0x80,0x25,0x70,0x72,0x59,0x85,0x73,0x7A,0x5A,0x86,0x20,0x26", outClusters: "0x82", deviceJoinName: "Z-Wave Shade"
>         fingerprint deviceId: "0x1107", inClusters: "0x5E,0x80,0x25,0x70,0x72,0x59,0x85,0x73,0x7A,0x5A,0x86,0x20,0x26", outClusters: "0x82", deviceJoinName: "Z-Wave Shade" 
> 	}
>     
>     preferences {
> 	    input "customLevel", "number", title: "Custom Level", required: true, defaultValue: 66, range: "0..100"
>         input "logging", "enum", title: "Log Level", required: false, defaultValue: "INFO", options: ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]
>     }
> 
> 	tiles(scale: 2) {
>     	multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
> 			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
> 				attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home9", backgroundColor:"#79b821", nextState:"turningOff"
> 				attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home9", backgroundColor:"#ffffff", nextState:"turningOn"
> 				attributeState "turningOn", label:'${name}', action:"switch.on", icon:"st.Home.home9", backgroundColor:"#79b821", nextState:"turningOff"
> 				attributeState "turningOff", label:'${name}', action:"switch.off", icon:"st.Home.home9", backgroundColor:"#ffffff", nextState:"turningOn"
> 			}
> 			
>             tileAttribute ("device.level", key: "SLIDER_CONTROL") {
> 				attributeState "level", action:"switch level.setLevel"
> 			}
> 		}
> 		multiAttributeTile(name:"switchDetails", type: "lighting", width: 6, height: 4, canChangeIcon: true){
> 			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
> 				attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home9", backgroundColor:"#79b821", nextState:"turningOff"
> 				attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home9", backgroundColor:"#ffffff", nextState:"turningOn"
> 				attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home9", backgroundColor:"#79b821", nextState:"turningOff"
> 				attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home9", backgroundColor:"#ffffff", nextState:"turningOn"
> 			}
>             
>             tileAttribute ("device.battery", key: "SECONDARY_CONTROL") {
> 				attributeState "default", label:'Battery: ${currentValue}%', action: "refresh.refresh"
> 			}
> 		}
>     
> 		standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
> 			state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
> 		}
>         
>         valueTile("ShadeLevel", "device.level", width: 2, height: 1) {
>         	state "level", label: 'Shade is ${currentValue}% up'
>         }
>         
>         controlTile("levelSliderControl", "device.level", "slider", width: 4, height: 1) {
>         	state "level", action:"switch level.setLevel"
>         }
>         
>         standardTile("sceneOne", "device.sceneOne", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
> 			state "default", label:'${currentValue}%', action:"sceneOne", icon: "st.Weather.weather14"
> 		}
>         
>         standardTile("sceneTwo", "device.sceneTwo", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
> 			state "default", label:"20%", action:"sceneTwo", icon: "st.Weather.weather14"
> 		}
>         
>         standardTile("sceneThree", "device.sceneThree", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
> 			state "default", label:"40%", action:"sceneThree", icon: "st.Weather.weather14"
> 		}
>         
>         standardTile("sceneFour", "device.sceneFour", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
> 			state "default", label:"60%", action:"sceneFour", icon: "st.Weather.weather14"
> 		}
>         
>         standardTile("sceneFive", "device.sceneFive", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
> 			state "default", label:"80%", action:"sceneFive", icon: "st.Weather.weather14"
> 		}
> 
> 		valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
> 			state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
> 		}
>         
>         standardTile("doPoll", "device.doPoll", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
> 			state "default", label:"Do Poll", action:"doPoll", icon: "st.Weather.weather14"
> 		}
>         
>         valueTile("LastActivity", "device.lastActivity", width: 6, height: 2) {
>         	state "default", label: 'Last Activity ${currentValue}'
>         }
>         
>         valueTile("LastConfigured", "device.lastConfigured", width: 6, height: 2) {
>         	state "default", label: 'Last Configured ${currentValue}'
>         }
>         
>         valueTile("LastPoll", "device.lastPoll", width: 6, height: 2) {
>         	state "default", label: 'Last Poll ${currentValue}'
>         }
>         
>         valueTile("LastBattery", "device.lastBattery", width: 6, height: 2) {
>         	state "default", label: 'Last Battery ${currentValue}'
>         }
> 
>         main(["switch", "level"])
>     	details(["switchDetails", "ShadeLevel", "levelSliderControl", "sceneOne", "sceneTwo", "sceneThree", "sceneFour", "sceneFive", "refresh", "top", "bottom"])
> 
> 	}
> }
> 
> def installed() {
> 	poll()
> }
> 
> def configure() {
>     delayBetween([
> 		def result = zwave.wakeUpV1.wakeUpNoMoreInformation().format(),
>         zwave.wakeUpV1.wakeUpIntervalSet(seconds:4 * 3600, nodeid:zwaveHubNodeId).format()
> 	])
> }
> 
> def updated() {
>     log("${getVersionStatementString()}", "DEBUG")
> 
> 	sendEvent(name: "sceneOne", value: customLevel, display: false , displayed: false)
>     log("Custom Level Selected: ${customLevel}.", "INFO")
>     log("Debug Level Selected: ${logging}.", "INFO")
>     
>     poll()
> }
> 
> def doPoll() {
> 	poll()
> }
> 
> def poll() {
> 	log("Polling...", "DEBUG")
>     
>     updateDeviceLastPoll(new Date())
>     
>     log("${getVersionStatementString()}", "DEBUG")
>     
>     configCheck()
>     
>     def commands = []
> 	
>     commands << zwave.switchMultilevelV1.switchMultilevelGet().format()
>     commands << zwave.batteryV1.batteryGet().format()
> 	
>     if (getDataValue("MSR") == null) {
> 		commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
> 	}
> 	
>     def result = delayBetween(commands,100)
>     
>     log("result = ${result}", "DEBUG")
>     
>     return result
> }
> 
> def refresh() {
> 	
>     log("Refreshing.", "DEBUG")
>     log("${getVersionStatementString()}", "DEBUG")
>     log("windowShade = ${device.currentValue('windowShade')}.", "INFO")
> 	
>     def commands = []
> 	commands << zwave.switchMultilevelV1.switchMultilevelGet().format()
>     commands << zwave.batteryV1.batteryGet().format()
> 	if (getDataValue("MSR") == null) {
> 		commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
> 	}
> 	def result = delayBetween(commands,100)
>     log("result = ${result}", "DEBUG")
>     
>     return result
> }
> 
> def parse(String description) {
> 	def result = null
> 	if (description != "updated") {
> 		log("parse() >> zwave.parse($description)", "DEBUG")
>         if(description.trim().endsWith("payload: 00 00 00")) {
>         	sendEvent(name: "level", value: 0, unit: "%")
>             log("Shade is down, setting level to 0%.", "DEBUG")
>         } else if(description.contains("command: 2603")) {
>         	def hexVal = description.trim()[-8..-7]
>             def movingHex = description.trim()[-2..-1]
>             log("hexVal = ${hexVal}.", "DEBUG")
>             try {
>                 def intVal = zigbee.convertHexToInt(hexVal)
>                 def movingInt = zigbee.convertHexToInt(movingHex)
>                 log("intVal = ${intVal}.", "DEBUG")
>                 
>                 if(movingInt == 0) {
>                 	log("Shade has stopped.", "INFO")
>                 } else if(movingInt == 254) {
>                 	log("Shade is moving.", "INFO")
>                 } else {
>                 	log("movingInt = ${movingInt}.", "INFO")
>                 }
>                 
>                 sendEvent(name: "level", value: intVal, unit: "%")
>             } catch(e) {
>             	log("Exception ${e}", "ERROR")
>             }
>         } else if(description.contains("command: 8003")) {
>         	log("Battery Reported.", "DEBUG")
>         }
>         
> 		def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
> 		if (cmd) {
> 			result = zwaveEvent(cmd)
> 		}
> 	}
> 	if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) {
> 		result = [result, response(zwave.basicV1.basicGet())]
> 		log("Was hailed: requesting state update", "DEBUG")
> 	} else {
> 		log("Parse returned ${result?.descriptionText}", "DEBUG")
> 	}
>     
> 	return result
> }
> 
> def configCheck() {
>     if(shouldReconfigure() == true || isConfigured() == false) {
>     	log("Reconfiguring the device as the state value has changed.", "DEBUG")
>         configure()
>         setStateVersion(getNewStateVersion())
>         state.configured = true
>         updateDeviceLastConfigured(new Date())
>     } else {
>     	log("Device already configured.", "DEBUG")
>     }
> }
> 
> def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
>         log("BatteryReport", "INFO")
>         def map = [ name: "battery", unit: "%" ]
>         if (cmd.batteryLevel == 0xFF) {  // Special value for low battery alert
>                 map.value = 1
>                 map.descriptionText = "${device.displayName} has a low battery"
>                 map.isStateChange = true
>         } else {
>                 map.value = cmd.batteryLevel
>         }
>         // Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
>         state.lastbatt = new Date().time
>         updateDeviceLastBattery(new Date())
>         createEvent(map)
> }
> 
> def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd) {
> 	log("WakeUpNotification", "INFO")
>     updateDeviceLastActivity(new Date())
>         def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
> 
>         if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) {
>                 result << response(zwave.batteryV1.batteryGet())
>                 result << response("delay 1200")  // leave time for device to respond to batteryGet
>         }
>         result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())
>         result
> }
> 
> def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) {
> 	dimmerEvents(cmd)
> }
> 
> def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {
> 	dimmerEvents(cmd)
> }
> 
> def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
> 	dimmerEvents(cmd)
> }
> 
> def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) {
> 	dimmerEvents(cmd)
> }
> 
> private dimmerEvents(hubitat.zwave.Command cmd) {
> 	def value = (cmd.value ? "on" : "off")
> 	def result = [createEvent(name: "switch", value: value)]
> 	if (cmd.value && cmd.value <= 100) {
> 		result << createEvent(name: "level", value: cmd.value, unit: "%")
> 	}
> 	return result
> }
> 
> def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) {
> 	log.debug "ConfigurationReport $cmd"
> 	def value = "when off"
> 	if (cmd.configurationValue[0] == 1) {value = "when on"}
> 	if (cmd.configurationValue[0] == 2) {value = "never"}
> 	createEvent([name: "indicatorStatus", value: value])
> }
> 
> def zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) {
> 	createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false])
> }
> 
> def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
> 	log.debug "manufacturerId:   ${cmd.manufacturerId}"
> 	log.debug "manufacturerName: ${cmd.manufacturerName}"
> 	log.debug "productId:        ${cmd.productId}"
> 	log.debug "productTypeId:    ${cmd.productTypeId}"
> 	def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
> 	updateDataValue("MSR", msr)
> 	updateDataValue("manufacturer", cmd.manufacturerName)
> 	createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
> }
> 
> def zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
> 	[createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())]
> }
> 
> def zwaveEvent(hubitat.zwave.Command cmd) {
> 	// Handles all Z-Wave commands we aren't interested in
>     log("Unhandled Event ${cmd}", "DEBUG")
> 	[:]
> }
> 
> def on() {
>     setLevel(99)
> }
> 
> def off() {
> 	setLevel(0)
> }
> 
> def setLevel(level) {
> 	log("setLevel(${level}).", "DEBUG")
>     
>     if(level >= 100) {
>     	level = getMaxLevel()
>     } else if(level < 0) {
>     	level = 0
>     }
>     
> 	if (level > 0 && level < 99) {
> 		sendEvent(name: "switch", value: "on")
>         sendEvent(name: "windowShade", value: "partially open")
> 	} else if(level == 0) {
> 		sendEvent(name: "switch", value: "off")
>         sendEvent(name: "windowShade", value: "closed")
> 	} else {
>     	sendEvent(name: "windowShade", value: "open")
>     }
>     
> 	sendEvent(name: "level", value: level, unit: "%")
> 	delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
> }
> 
> def setLevel(value, duration) {
> 	setLevel(value)
> }
> 
> def open() {
> 	setLevel(getMaxLevel())
> }
> 
> def close() {
> 	setLevel(0)
> }
> 
> def presetPosition() {
> 	setLevel(customLevel)
> }
> 
> def levelOpenClose(value) {
>     log("levelOpenClose called with value ${value}.", "DEBUG")
>     if (value) {
>         on()
>     } else {
>         off()
>     }
> }
> 
> /************ Begin Logging Methods *******************************************************/
> 
> def determineLogLevel(data) {
>     switch (data?.toUpperCase()) {
>         case "TRACE":
>             return 0
>             break
>         case "DEBUG":
>             return 1
>             break
>         case "INFO":
>             return 2
>             break
>         case "WARN":
>             return 3
>             break
>         case "ERROR":
>         	return 4
>             break
>         default:
>             return 1
>     }
> }
> 
> def log(data, type) {
>     data = "Z-Wave Shade -- v${dhVersion()} --  ${data ?: ''}"
>         
>     if (determineLogLevel(type) >= determineLogLevel(settings?.logging ?: "INFO")) {
>         switch (type?.toUpperCase()) {
>             case "TRACE":
>                 log.trace "${data}"
>                 break
>             case "DEBUG":
>                 log.debug "${data}"
>                 break
>             case "INFO":
>                 log.info "${data}"
>                 break
>             case "WARN":
>                 log.warn "${data}"
>                 break
>             case "ERROR":
>                 log.error "${data}"
>                 break
>             default:
>                 log.error "Z-Wave Shade -- Invalid Log Setting"
>         }
>     }
> }
> 
> /************ End Logging Methods *********************************************************/
> 
> def sceneOne() {
>     setLevel(customLevel)
> }
> 
> def sceneTwo() {
>     setLevel(20)
> }
> 
> def sceneThree() {
>     setLevel(40)
> }
> 
> def sceneFour() {
>     setLevel(60)
> }
> 
> def sceneFive() {
>     setLevel(80)
> }
> 
> def getMaxLevel() {
> 	return 99
> }
> 
> def isConfigured() {
> 	log("${getVersionStatementString()}", "DEBUG")
> 	if (state.configured == null || state.configured == false) {
>     	return false
> 	} else {
>     	return true
>     }
> }
> 
> def getStateVersion() {
> 	if(state.version != null) {
> 		return state.version
>     } else {
>     	return 0
>     }
> }
> 
> def setStateVersion(val) {
> 	log("Updating State Version to ${val}.", "INFO")
> 	state.version = val
> }
> 
> def getNewStateVersion() {
> 	return 10
> }
> 
> def getVersionStatementString() {
> 	return "Current state version is ${getStateVersion()} and new state version is ${getNewStateVersion()}."
> }
> 
> def shouldReconfigure() {
> 	if(getNewStateVersion() > getStateVersion()) {
>     	return true
>     } else {
>     	return false
>     }
> }
> 
> def getBattery() {
> 	def commands = []
>     commands << zwave.batteryV1.batteryGet().format()
> 	def result = delayBetween(commands,100)
>     log("result = ${result}", "DEBUG")
>     return result
> }
> 
> def updateDeviceLastActivity(lastActivity) {
> 	def finalString = lastActivity?.format('MM/d/yyyy hh:mm a',location.timeZone)
> 	sendEvent(name: "lastActivity", value: finalString, display: false , displayed: false)
> }
> 
> def updateDeviceLastConfigured(lastConfigured) {
> 	def finalString = lastConfigured?.format('MM/d/yyyy hh:mm a',location.timeZone)
>     log("Raising lastConfigured event with ${finalString}.", "INFO")
> 	sendEvent(name: "lastConfigured", value: finalString, display: false , displayed: false)
> }
> 
> def updateDeviceLastPoll(lastPoll) {
> 	def finalString = lastPoll?.format('MM/d/yyyy hh:mm a',location.timeZone)    
> 	sendEvent(name: "lastPoll", value: finalString, display: false , displayed: false)
> }
> 
> def updateDeviceLastBattery(lastBattery) {
> 	def finalString = lastBattery?.format('MM/d/yyyy hh:mm a',location.timeZone)    
> 	sendEvent(name: "lastBattery", value: finalString, display: false , displayed: false)
> }
1 Like

+1 for thermostat tile. Pretty please :slight_smile:
In the meantime, what is the current way people set thermostat temperature over the internet?

I thought you'd never ask. :slight_smile:

I use a virtual dimmer on Hubitat, which I have "synced" to SmartThings via the community Other Hub integration. On SmartThings, I have a virtual thermostat and a webCoRE piston that syncs the thermostat setpoint with the "dimmer level" (luckily, my setpoint is always somewhere between 0 and 100, so this awkward way of syncing these values works pretty well). While you don't need this to set the temperature, I also like to have the current temperature in the thermostat device set to the correct value, so I have another dimmer that unidirectionally syncs this value from the "real" Hubitat thermostat to the virtual dimmer and then to ST and the virtual thermostat--all so I can ask Alexa to change the thermostat or what the current thermostat temperature is.

But if you just want to change the setpoint from a dashboard or Alexa and don't mind using a dimmer instead of a thermostat tile (and needing to say "Alexa, dim XYZ to 60%" instead of "...set the thermostat to 60"), you can just create a virtual dimmer in Hubitat. Last I checked, Rule Machine didn't have a way to sync a dimmer level to a thermostat setpoint or temperature (can't imagine why....), so I had to create an app myself. This appears below:

/**
 *  Thermosat/Dimmer Sync Helper
 *
 *  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: "Thermostat/Dimmer Sync Helper",
namespace: "RMoRobert",
author: "RMoRboert",
description: "Syncs the setpoint of a thermostat and the dimmer level of a virtual switch (bidirectional) and the thermostat temperature to a virtual dimmer (unidirectional), intended for using the virtual devices with other platforms that do not natively support thermostat integration",
category: "Convenience",
iconUrl: "",
iconX2Url: "",
iconX3Url: ""
)

preferences {
	mainPage()
}

def mainPage() {
	page(name:"mainPage", title:"Settings", install: true, uninstall: true) {
		section("Choose thermostat and virtual dimmers to sync with") {
			input (name:"thermostat", type: "capability.thermostat", title: "Thermostat", required: true, multiple: false)
			input (name:"setpointDimmer", type: "capability.switchLevel", title: "Setpoint dimmer", required: true, multiple: false)
			input (name:"tempDimmer", type: "capability.switchLevel", title: "Temperature dimmer", required: true, multiple: false)
		}
		section("Logging", hideable: true, hidden: true) {
			input ("debugLogging", "bool", title: "Enable verbose/debug logging")
		}
	}
}

def installed() {
	initialize()
}

def updated() {
	unsubscribe()
	unschedule()
	initialize()
}

def initialize() {
	log.debug "Initializing"
	subscribe(thermostat, "thermostatSetpoint", realSetpointHandler)
	subscribe(thermostat, "temperature", tempHandler)
	subscribe(setpointDimmer, "level", virtualSetpointHandler)
}

def realSetpointHandler(evt) {
    if (debugLogging) log.debug("Thermostat setpoint changed...")
    def newSetpoint = thermostat.currentValue("thermostatSetpoint") 
    def currLevel = setpointDimmer.currentLevel
    if (currLevel > newSetpoint - 0.5 && currLevel < newSetpoint + 0.5) {
        log.debug "Virtual dimmer not changed because setpoint of ${newSetpoint} is already close to virtual dimmer level of ${currLevel}"
    }
    else {
        setpointDimmer.setLevel(newSetpoint)
        log.debug "Changed virtual setpoint dimmer level to ${newSetpoint} because thermostat setpoint changed"
    }
    if (debugLogging) log.debug("...done handling thermostat setpoint change.")
}

def tempHandler(evt){
    if (debugLogging) log.debug("Thermostat temperature changed...")
    def newTemp = thermostat.currentValue("temperature")
	newTemp = Math.round(newTemp)
    tempDimmer.setLevel(newTemp)
    log.debug "Changed virtual temperature dimmer level to ${newTemp} because thermostat temperature changed"
    if (debugLogging) log.debug("...done handling thermostat temperature change.")
}

def virtualSetpointHandler(evt) {
    if (debugLogging) log.debug("Virtual setpoint dimmer level changed...")
    def targetSetpoint = setpointDimmer.currentLevel
    def currSetpoint = thermostat.currentValue("thermostatSetpoint") 
    def thermostatMode = thermostat.currentValue("thermostatMode")   
    if (debugLogging) log.debug("Target setpoint = ${targetSetpoint}; current setpoint = ${currSetpoint}; thermostat mode = ${thermostatMode}")
    if (currSetpoint > targetSetpoint - 0.5 && currSetpoint < targetSetpoint + 0.5) {
        log.debug "Thermostat not changed because setpoint of ${currSetpoint} is already close to virtual dimmer target of ${targetSetpoint}"
    }
    else {
        if (thermostatMode == "cool") {
            thermostat.setCoolingSetpoint(targetSetpoint)
			log.debug "Set thermostat cooling setpoint to ${targetSetpoint} because virtual dimmer changed"
        }
        else if (thermostatMode == "heat") {
            thermostat.setHeatingSetpoint(targetSetpoint)
			log.debug "Set thermostat heating setpoint to ${targetSetpoint} because virtual dimmer changed"
        }
        else {
            log.debug "Thermostat not adjusted because not in heat or cool mode (mode = ${thermostatMode})"
        }
    }
    if (debugLogging) log.debug("...done handling virtual setpoint dimmer change.")
}

I've only tested this with my thermostat (a Zen Thermostat) in heating mode (I wrote code to make it work with heat or cool but haven't really tested, and if you have more modes you may need further modification), but it works for me. If you don't care to sync the thermostat temperature (actual temperature, not setpoint) with a virtual dimmer, you can probably remove that part, but I just wanted one app that did both since I do need it for my ST integration. I can share the ST/webCoRE side of that if you're interested in the full picture, but this should work for Alexa (awkwardly) or a Hubitat Dashboard (also awkwardly).

PS - For a thermostat dashboard, I usually use SharpTools, which has good thermostat support. I hope Hubitat Dashboard does some day too since I'd like a local solution, along with full Alexa support (since I'd like a less awkward solution).

2 Likes

I use ecobee, so local vs cloud makes no difference to me (I'm forced to cloud either way).

But I would very much like a dashboard tile for it just so I could have my HVAC controls on the same HE dashboards I have my lighting and security on.

Wow, thanks! I can see myself using your nice dimmer trick out of desperation :slight_smile:

Love to see a thermostat tile in the Dashboard... thanks

I must admit this is a very rare case where I'm disappointed with the Hubitat team. A thermostat tile would seem a fairly common requirement for a dashboard, and has been requested many times. Maybe it's harder to do technically than I imagine.

Thermostat tile although it may seem simple, is quite complex in the different use cases of auto, heat, cool, fan mode, multiple setpoints, temperatures, etc. It breaks with the single action model that dashboard was originally set up to serve as.

Regardless, it is on the list and will be added down the road.

1 Like

Fair enough. Things always seem easier to do when you don't understand how they work!

To be fair, I would argue that was a very BAD model from the get go, and counter to the direction most dashboards are going... :smile:

Regardless, a thermostat tile will be very welcome (and appreciated!) when it arrives.

1 Like

I don't understand the single action comment really. I'm looking at my Sonos dashboard tile with play, pause, skip, rewind, volume controls etc. Is that very different to the requirements of a thermostat tile?

Having implemented a thermostat tile, I can say that it is indeed one of the more complex tiles.

In the case of a music tile, the the forward button is always forward() and the previous button is always previous()... that's not necessarily the case for a thermostat when you press the increase temperature button. :stuck_out_tongue:

At least in the case of SharpTools.io, there's a lot of stuff that happens behind the scenes when you press the increment temperature button to make it feel like an intuitive and smooth experience. For example, just pushing that increase temperature button includes determining what mode you are in and thus if the cooling setpoint or heating setpoint should be changed, debouncing events and determining when the value should actually be changed (allowing you to tap up a few times before the temperature change is sent), temporarily decoupling the 'local' setpoint you changed from the reported setpoint and then recoupling appropriately, etc.

2 Likes

In case anyone is interested in something similar, I dedicated a whole dashboard to thermostat control, with two virtual buttons and two RM rules to handle the increase and decreases. Seems to work fine.

4 Likes

I have no doubt of that!! A lot of parameters and modes to consider after all.

It would maybe make sense to have different tiles for heating and cooling stats. A lot of people (everyone in the UK for example) only need heating anyway, and if it complicates things to combine them I don't imagine people that need both would mind having 2 seperate tiles.

Personally all I want is a single heating thermostat tile (maybe 2 tiles wide) with Actual & Set temperature, up/down temperature buttons and change of colour (icon and/or background) when in heating mode. At the moment it takes me 3 attribute tiles, 2 virtual switches, 3 switch tiles and 6 image tiles to achieve something that looks half reasonable. Maybe an "all singing all dancing" thermostat tile is tricky but I really don't see that a basic heating one like that should be. Josh's Sharptools one is brilliant but I really do prefer a local dashboard.

Since it's been over another month with no activity on this, just wanted to see if this is going to be implemented with Dashboards 2.0.

Yes, thermostat tile is in the upcoming update to dashboard.

4 Likes