Dashboard feature request - thermostat template

Can we get a thermostat template in the dashboard? What I am looking for is a widget to display current temperature, current heatSetpoint and battery status and have two arrow buttons (up and down) to increase and decrease the current heatSetpoint.
Similar to the main tile of the ST DTH for the device:

6 Likes

I would like to see both heat and cool setpoints, temp, and humidity in a single thermostat tile. Donā€™t really care until Ecobee integration is available, but would eventually like this.

Thatā€™s a tall order. I donā€™t think thereā€™s room in a single tile for all that unless you start making the tile double wide or tall. Maybe one tile for manual control (hot/cold setpoints) and a separate one for heat/cool/auto? Something to ponder.

1 Like

Agree, I wasn't picturing it in a standard style tile. I was picturing it in a 2 row three column tile similar to the way they are in ActionTiles.

1 Like

Yeah, even the thermostat tile from ActionTiles is twice as wide as a standard tile. Although AT doesnā€˜t have the up and down keys for adjusting the setpoints, they only have the heat/cool/Auto and Fan settings, which are not needed on a TRV. So, maybe it is a good idea to provide different thermostat tiles for different functionalit or allow for selecting which functionality from a given set of options you want to have in the tile.
So, maybe a thermostat tile that provides three functions of a set of letā€˜s say six. Then you could place either one or two tiles depending on the functionality and control you want to have. In my TRV case I would be fine with a tile showing the temperature and giving me the heatSetpoint controls. And somebody with a standard thermostat like Ecobee may want the cool/heat/Auto and fan controls as well on a separate tile.
I agree that this is probably a big request. But a thermostat is more feature rich than a contact, switch or light bulb. There are just more control options that would be expected on a dashboard.

Here is picture of the ActionActiontiles thermostat tile for reference.

It displays this when you click the three dots

This is how I'm currently rollin

2 Likes

Ah, yes, you are right it does have the "+" and "-" for controlling temperature. The one I looked at when I wrote above was a tile that wasn't connected to a "thing" anymore. So, it was messed up. I just checked with a working device and it does have everything I want to see.

22

BTW: an additional piece of information may be good to know when creating such a tile in the hubitat dashboard. The AT settings for adding such a tile gives you the ability to choose whether it is a cooling, heating or dual thermostat in order to limit controls to what you need. In my case with a TRV it is just a Heating Thermostat.

1 Like

Maybe a bigger question than just the dashboard and thermostatsā€¦what about the hold status? I was looking at the Centralite Pearl Smartthings device handler and I know in the past it had support for hold but does not now. I was able to track down an older device driver that does have the hold logic and plan to play with that and see what is going on. My husband likes to do the ā€œhold at the thermostatā€ so any device driver needs to be able to recognize that the temperature is on hold and not change it. I also raised this question to the Vera hub support and basically it sounded like they didnā€™t know what I was talking about :slight_smile:

So long story short, on a thermostat dashboard I would also like to see (and be able to set if that is possible) the hold status.

2 Likes

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.