Virtual Thermostat Limits device request

I am trying to use a virtual thermostat bridge between Google Home and a BWA Spa. I want to ask google to set the Spa temperature. Google home will not connect to the BWA Spa device (it just ignores it if I try to connect it). My thought is to use a virtual thermostat with rule machine to control the spa. I've run into problem in that google home seems have a limit to how high it will set the temperature. When I searched the web and found this info:

Adjusting Temperature Limits for Hubitat Thermostats in Google Home

To change the temperature limits for a Hubitat thermostat that is integrated with Google Home, follow these steps:

Required Attributes and Commands

Ensure that your thermostat's driver includes the following attributes and commands:

  • Attributes:
    • minHeatingSetpoint
    • maxHeatingSetpoint

It seems like the virtual driver may not have these because I set the virtual thermostat to 104F in Hubitat. However, google says it can't set it that high around 96degrees.

Are these set in the code? (I assume there are similar cooing limits)

It would be great if they were exposed in the UI so they could be adjusted for each purpose.

You can try this. I just added min/max heat/cool attributes and commands to set them.

Virtual Thermostat with Min and Max Setpoints
/*
	Virtual Thermostat

	Copyright 2016 -> 2023 Hubitat Inc.  All Rights Reserved
	
	Added min and max heating setpoint attribute

*/

metadata {
	definition (
			name: "Virtual Thermostat",
			namespace: "hubitat",
			author: "Kevin L., Mike M., Bruce R."
	) {
		capability "Actuator"
		capability "Sensor"
		capability "Temperature Measurement"
		capability "Thermostat"

		attribute "supportedThermostatFanModes", "JSON_OBJECT"
		attribute "supportedThermostatModes", "JSON_OBJECT"
		attribute "hysteresis", "NUMBER"
		attribute "minHeatingSetpoint", "NUMBER"
		attribute "maxHeatingSetpoint", "NUMBER"
		attribute "minCoolingSetpoint", "NUMBER"
		attribute "maxCoolingSetpoint", "NUMBER"		

		// Commands needed to change internal attributes of virtual device.
		command "setTemperature", ["NUMBER"]
		command "setMinHeatingSetpoint", ["NUMBER"]
		command "setMaxHeatingSetpoint", ["NUMBER"]
		command "setMinCoolingSetpoint", ["NUMBER"]
		command "setMaxCoolingSetpoint", ["NUMBER"]		
		command "setThermostatOperatingState", ["ENUM"]
		command "setThermostatSetpoint", ["NUMBER"]
		command "setSupportedThermostatFanModes", ["JSON_OBJECT"]
		command "setSupportedThermostatModes", ["JSON_OBJECT"]
	}

	preferences {
		input( name: "hysteresis",type:"enum",title: "Thermostat hysteresis degrees", options:["0.1","0.25","0.5","1","2"], description:"", defaultValue: 0.5)
		input( name: "logEnable", type:"bool", title: "Enable debug logging",defaultValue: false)
		input( name: "txtEnable", type:"bool", title: "Enable descriptionText logging", defaultValue: true)
	}
}
import groovy.json.JsonOutput

def installed() {
	log.warn "installed..."
	initialize()
}

def updated() {
	log.info "updated..."
	log.warn "debug logging is: ${logEnable == true}"
	log.warn "description logging is: ${txtEnable == true}"
	if (logEnable) runIn(1800,logsOff)
	initialize()
}

def initialize() {
	if (state?.lastRunningMode == null) {
		sendEvent(name: "temperature", value: convertTemperatureIfNeeded(68.0,"F",1))
		sendEvent(name: "thermostatSetpoint", value: convertTemperatureIfNeeded(68.0,"F",1))
		sendEvent(name: "heatingSetpoint", value: convertTemperatureIfNeeded(68.0,"F",1))
		sendEvent(name: "coolingSetpoint", value: convertTemperatureIfNeeded(75.0,"F",1))
		state.lastRunningMode = "heat"
		updateDataValue("lastRunningMode", "heat")
		setThermostatOperatingState("idle")
		setSupportedThermostatFanModes(JsonOutput.toJson(["auto","circulate","on"]))
		setSupportedThermostatModes(JsonOutput.toJson(["auto", "cool", "emergency heat", "heat", "off"]))
		off()
		fanAuto()
	}
	sendEvent(name: "hysteresis", value: (hysteresis ?: 0.5).toBigDecimal())
}

def logsOff(){
	log.warn "debug logging disabled..."
	device.updateSetting("logEnable",[value:"false",type:"bool"])
}

def manageCycle(){
	def ambientTempChangePerCycle = 0.25
	def hvacTempChangePerCycle = 0.75

	def hysteresis = (hysteresis ?: 0.5).toBigDecimal()

	def coolingSetpoint = (device.currentValue("coolingSetpoint") ?: convertTemperatureIfNeeded(75.0,"F",1)).toBigDecimal()
	def heatingSetpoint = (device.currentValue("heatingSetpoint") ?: convertTemperatureIfNeeded(68.0,"F",1)).toBigDecimal()
	def temperature = (device.currentValue("temperature") ?: convertTemperatureIfNeeded(68.0,"F",1)).toBigDecimal()

	def thermostatMode = device.currentValue("thermostatMode") ?: "off"
	def thermostatOperatingState = device.currentValue("thermostatOperatingState") ?: "idle"

	def ambientGain = (temperature + ambientTempChangePerCycle).setScale(2)
	def ambientLoss = (temperature - ambientTempChangePerCycle).setScale(2)
	def coolLoss = (temperature - hvacTempChangePerCycle).setScale(2)
	def heatGain = (temperature + hvacTempChangePerCycle).setScale(2)

	def coolingOn = (temperature >= (coolingSetpoint + hysteresis))
	if (thermostatOperatingState == "cooling") coolingOn = temperature >= (coolingSetpoint - hysteresis)

	def heatingOn = (temperature <= (heatingSetpoint - hysteresis))
	if (thermostatOperatingState == "heating") heatingOn = (temperature <= (heatingSetpoint + hysteresis))
	
	if (thermostatMode == "cool") {
		if (coolingOn && thermostatOperatingState != "cooling") setThermostatOperatingState("cooling")
		else if (thermostatOperatingState != "idle") setThermostatOperatingState("idle")
	} else if (thermostatMode == "heat") {
		if (heatingOn && thermostatOperatingState != "heating") setThermostatOperatingState("heating")
		else if (thermostatOperatingState != "idle") setThermostatOperatingState("idle")
	} else if (thermostatMode == "auto") {
		if (heatingOn && coolingOn) log.error "cooling and heating are on- temp:${temperature}"
		else if (coolingOn && thermostatOperatingState != "cooling") setThermostatOperatingState("cooling")
		else if (heatingOn && thermostatOperatingState != "heating") setThermostatOperatingState("heating")
		else if ((!coolingOn || !heatingOn) && thermostatOperatingState != "idle") setThermostatOperatingState("idle")
	}
}



// Commands needed to change internal attributes of virtual device.
def setTemperature(temperature) {
	logDebug "setTemperature(${temperature}) was called"
	sendTemperatureEvent("temperature", temperature)
	runIn(1, manageCycle)
}

def setHumidity(humidity) {
	logDebug "setHumidity(${humidity}) was called"
	sendEvent(name: "humidity", value: humidity, unit: "%", descriptionText: getDescriptionText("humidity set to ${humidity}%"))
}

def setThermostatOperatingState (operatingState) {
	logDebug "setThermostatOperatingState (${operatingState}) was called"
	updateSetpoints(null,null,null,operatingState)
	sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: getDescriptionText("thermostatOperatingState set to ${operatingState}"))
}

def setSupportedThermostatFanModes(fanModes) {
	logDebug "setSupportedThermostatFanModes(${fanModes}) was called"
	// (auto, circulate, on)
	sendEvent(name: "supportedThermostatFanModes", value: fanModes, descriptionText: getDescriptionText("supportedThermostatFanModes set to ${fanModes}"))
}

def setSupportedThermostatModes(modes) {
	logDebug "setSupportedThermostatModes(${modes}) was called"
	// (auto, cool, emergency heat, heat, off)
	sendEvent(name: "supportedThermostatModes", value: modes, descriptionText: getDescriptionText("supportedThermostatModes set to ${modes}"))
}


def auto() { setThermostatMode("auto") }

def cool() { setThermostatMode("cool") }

def emergencyHeat() { setThermostatMode("heat") }

def heat() { setThermostatMode("heat") }
def off() { setThermostatMode("off") }

def setThermostatMode(mode) {
	sendEvent(name: "thermostatMode", value: "${mode}", descriptionText: getDescriptionText("thermostatMode is ${mode}"))
	setThermostatOperatingState ("idle")
	updateSetpoints(null, null, null, mode)
	runIn(1, manageCycle)
}

def fanAuto() { setThermostatFanMode("auto") }
def fanCirculate() { setThermostatFanMode("circulate") }
def fanOn() { setThermostatFanMode("on") }

def setThermostatFanMode(fanMode) {
	sendEvent(name: "thermostatFanMode", value: "${fanMode}", descriptionText: getDescriptionText("thermostatFanMode is ${fanMode}"))
}

def setThermostatSetpoint(setpoint) {
	logDebug "setThermostatSetpoint(${setpoint}) was called"
	updateSetpoints(setpoint, null, null, null)
}

def setCoolingSetpoint(setpoint) {
	logDebug "setCoolingSetpoint(${setpoint}) was called"
	updateSetpoints(null, null, setpoint, null)
}

def setHeatingSetpoint(setpoint) {
	logDebug "setHeatingSetpoint(${setpoint}) was called"
	updateSetpoints(null, setpoint, null, null)
}

private setMinHeatingSetpoint(val) {
	sendEvent(name: "minHeatingSetpoint", value: val, unit: "°${getTemperatureScale()}", descriptionText: getDescriptionText("minHeatingSetpoint is ${val} °${getTemperatureScale()}"), isStateChange: true)
}

private setMaxHeatingSetpoint(val) {
	sendEvent(name: "maxHeatingSetoint", value: val, unit: "°${getTemperatureScale()}", descriptionText: getDescriptionText("maxHeatingSetoint is ${val} °${getTemperatureScale()}"), isStateChange: true)
}

private setMinCoolingSetpoint(val) {
	sendEvent(name: "minCoolingSetpoint", value: val, unit: "°${getTemperatureScale()}", descriptionText: getDescriptionText("minCoolingSetpoint is ${val} °${getTemperatureScale()}"), isStateChange: true)
}

private setMaxCoolingSetpoint(val) {
	sendEvent(name: "maxCoolingSetoint", value: val, unit: "°${getTemperatureScale()}", descriptionText: getDescriptionText("maxCoolingSetoint is ${val} °${getTemperatureScale()}"), isStateChange: true)
}

private updateSetpoints(sp = null, hsp = null, csp = null, operatingState = null){
	if (operatingState in ["off"]) return
	if (hsp == null) hsp = device.currentValue("heatingSetpoint",true)
	if (csp == null) csp = device.currentValue("coolingSetpoint",true)
	if (sp == null) sp = device.currentValue("thermostatSetpoint",true)

	if (operatingState == null) operatingState = state.lastRunningMode

	def hspChange = isStateChange(device,"heatingSetpoint",hsp.toString())
	def cspChange = isStateChange(device,"coolingSetpoint",csp.toString())
	def spChange = isStateChange(device,"thermostatSetpoint",sp.toString())
	def osChange = operatingState != state.lastRunningMode

	def newOS
	def descriptionText
	def name
	def value
	def unit = "°${location.temperatureScale}"
	switch (operatingState) {
		case ["pending heat","heating","heat"]:
			newOS = "heat"
			if (spChange) {
				hspChange = true
				hsp = sp
			} else if (hspChange || osChange) {
				spChange = true
				sp = hsp
			}
			if (csp - 2 < hsp) {
				csp = hsp + 2
				cspChange = true
			}
			break
		case ["pending cool","cooling","cool"]:
			newOS = "cool"
			if (spChange) {
				cspChange = true
				csp = sp
			} else if (cspChange || osChange) {
				spChange = true
				sp = csp
			}
			if (hsp + 2 > csp) {
				hsp = csp - 2
				hspChange = true
			}
			break
		default :
			return
	}

	if (hspChange) {
		value = hsp
		name = "heatingSetpoint"
		descriptionText = "${device.displayName} ${name} was set to ${value}${unit}"
		if (txtEnable) log.info descriptionText
		sendEvent(name: name, value: value, descriptionText: descriptionText, unit: unit, isStateChange: true)
	}
	if (cspChange) {
		value = csp
		name = "coolingSetpoint"
		descriptionText = "${device.displayName} ${name} was set to ${value}${unit}"
		if (txtEnable) log.info descriptionText
		sendEvent(name: name, value: value, descriptionText: descriptionText, unit: unit, isStateChange: true)
	}
	if (spChange) {
		value = sp
		name = "thermostatSetpoint"
		descriptionText = "${device.displayName} ${name} was set to ${value}${unit}"
		if (txtEnable) log.info descriptionText
		sendEvent(name: name, value: value, descriptionText: descriptionText, unit: unit, isStateChange: true)
	}

	state.lastRunningMode = newOS
	updateDataValue("lastRunningMode", newOS)
}

def setSchedule(schedule) {
	sendEvent(name: "schedule", value: "${schedule}", descriptionText: getDescriptionText("schedule is ${schedule}"))
}

private sendTemperatureEvent(name, val) {
	sendEvent(name: "${name}", value: val, unit: "°${getTemperatureScale()}", descriptionText: getDescriptionText("${name} is ${val} °${getTemperatureScale()}"), isStateChange: true)
}


def parse(String description) {
	logDebug "$description"
}


private logDebug(msg) {
	if (settings?.logEnable) log.debug "${msg}"
}

private getDescriptionText(msg) {
	def descriptionText = "${device.displayName} ${msg}"
	if (settings?.txtEnable) log.info "${descriptionText}"
	return descriptionText
}

That looks like an AI result, not a real web search (but yes, Google is going to push these on you when you try to just do a search). Did it provide sources?

...because this is absolutely made up. :slight_smile: (At least on the Hubitat side as it claims. There may be something that can be configured in the data it sends to Google, but that's why knowing more about where it "thinks" this came from would be helpful.)

2 Likes