[RELEASE] New Virtual Thermostat With Device

The VID I set it as is radiator-thermostat but there are other VIDs for thermostats you can find here. the vid is located in the metadata.

I don’t think thermostats support .5 temps but if they did you’d have to change the temperature to a string both in the dth and the smartapp.

1 Like

thanks, i have changed it to generic-radiator-thermostat-2 in metadata and now i can set it up from 4 to 28 C and i have only heating option. Tested with a virtual switch both manually and in automation and all works perfect, cheers
you should probably use that VID default for others that want to set up lower anti freezing temps if they leave their home for longer times
PS: For those reading this, you need to re-add your thermostats after editing the device handler in order for changes to take effect

Hi!I cannot install the Smart App ! Pls help
Which steps??

Hi. I have the Virtual thermostat app set up with a Samsung motion sensor and I’m using the app on a ceiling fan to cool an area. Everything works except that the app sets the fan Speed to 66%. If I try to increase the fan speed, the virtual Thermometer changes the speed back to 66%. Is there a way to make the app set the speed to my choice? Or even set the speed based on temperature?

I managed to get the dual heating/cooling working without the “Emergency Temp” error listed above.
In the Device SmartApp, add the defs getCoolingStatus & shouldCoolingBeOn and then:

def handleChange() {
def thermostat = getThermostat()

//update device
thermostat.setHeatingStatus(getHeatingStatus(thermostat))
thermostat.setCoolingStatus(getCoolingStatus(thermostat))
thermostat.setVirtualTemperature(getAverageTemperature())

if(thermostat.currentValue('thermostatMode') == "off") {
//set outlet off
	outlets.off()

}

if(thermostat.currentValue('thermostatMode') == "heat") {
//set heater outlet
if(shouldHeatingBeOn(thermostat)) {
	outlets.on()
} else {
	outlets.off()
}

} else {

if(thermostat.currentValue('thermostatMode') == "cool") {
//set cooler outlet
if(shouldCoolingBeOn(thermostat)) {
	outlets.on()
} else {
	outlets.off()
}

}
}
}

Can someone please make this available to get updates? I keep getting "you don’t have access message when trying to add repo in IDE

which version is the a patch to? I need cooling, so instead of doing everything myself I’m trying to find the latest version that supports cooling and build on that.

After reading the code a bit, I get where this is coming from. I’m working on adding cooling outlets alongside heating outlets and controlling the thermostatMode as off/heat/cool/auto. Really shouldn’t be too complicated, but as this is my first trial at doing any kind of smarttthings coding, I may be wrong.

I have forked my changes here.

Use the SmartApp With Device and DTH.

Thanks, I did a quick and dirty manual merge of your changes into my changes here: https://github.com/steffennissen/SmartThings-VirtualThermostat-WithDTH

The thermostatOperatingState doesn’t really work with both cooling and heating, I’ll look into fixing that.

Also the heatingSetpoint/coolingSetpoint/thermostatSetpoint are currently forced to always be the same, which doesn’t really make much sense from a thermostat perspective, but in this case there is a tolerance factor which means that it kind of still works. Not at the top of my list right now, but I think it would make sense to get it fixed.

Managed to get cooling/heating/auto to work and get the state to get updated correctly. Also got the setpoint, heatingsetpoint and coolingsetpoint to be independent. On top of this I updated the UI a bit, so it now looks pretty nice. Still have a few additional changes that I want to make, but it’s fairly usable.

It’s all here: https://github.com/steffennissen/SmartThings-VirtualThermostat-WithDTH

This is how it looks like:

I updated my old Virtual Thermostat to your code. In Fahrenheit the minimum temperature for cooling is 71.7 or 72.7. I’m not sure which. The adjustment on on the right side of the main field goes down to 71.7, while the “cool temperature” , all the way on the right in the second column, reads 72.7. They adjust simultaneously regardless of which you adjust and they cannot go below those values. They should be able to get down to 60ºF.

Additionally, with Fahrenheit adjustments it should probably adjust 0.5 or 1.0 degrees per step, rather than 0.1 like celcius prefers.

This might be my fault since I upgraded code rather than uninstalling the old VT and installing yours. I appreciate your work and just wanted to let you know.

Did a few changes since then and added a few more parameters, also the arrows in the thermostat is now doing 0.5 increments. I would imagine that it’s still probably not too great with Fahrenheit, I’ll see if I can take a look at that over the weekend

The confusion between the 3 set points has also been fixed, smartthings has 3 set points, whereas most other systems has 2 (cooling/heating), I’ve now gotten rid of the 3rd one, so it’s only heating and cooling shown now.

The additional Cooling feature works great! and I like the new Device Handler interface as well. I use this in a °F environment and all temps report correctly with no manual overwriting. Hopefully the original OP can merge this into his code in order to maintain updates, as I had to copy/paste your updated code over the original.
The only item I’d like to see is the ability to set a delay before turning the heater/cooler off when a contact is opened. It is currently instant. My setup uses an inside contact sensor (small room AC unit cooling a small room). Anytime anyone walks into the room, the unit shuts off.
Is that a possibility?

1 Like

@Steffen_Nissen

I have added a fan speed settings to the cooling. I’d love for you to add this to your base. Unfortunately, while I could figure out the programming, I don’t know how to submit this change to you. My code is below which consists of a new function (at end of the file) and a few lines in cool().

definition(
name: “Virtual Thermostat With Device”,
namespace: “piratemedia/smartthings”,
author: “Eliot S.”,
description: “Control a heater in conjunction with any temperature sensor, like a SmartSense Multi.”,
category: “Green Living”,
iconUrl: “https://raw.githubusercontent.com/eliotstocker/SmartThings-VirtualThermostat-WithDTH/master/logo-small.png”,
iconX2Url: “https://raw.githubusercontent.com/eliotstocker/SmartThings-VirtualThermostat-WithDTH/master/logo.png”,
parent: “piratemedia/smartthings:Virtual Thermostat Manager”,
)

preferences {
section(“Choose a temperature sensor(s)… (If multiple sensors are selected, the average value will be used)”){
input “sensors”, “capability.temperatureMeasurement”, title: “Sensor”, multiple: true
}
section("Select the heater outlet(s)… "){
input “heating_outlets”, “capability.switch”, title: “Heating Outlets”, multiple: true
}
section("Select the cooling outlet(s)… "){
input “cooling_outlets”, “capability.switch”, title: “Cooling Outlefts”, multiple: true
}
section(“Only heat/cool when contact(s) aren’t open (optional, leave blank to not require contact sensor)…”){
input “motion”, “capability.contactSensor”, title: “Contact”, required: false, multiple: true
}
section(“Never go below this temperature: (optional)”){
input “emergencyHeatingSetpoint”, “decimal”, title: “Emergency Min Temp”, required: false
}
section(“Never go above this temperature: (optional)”){
input “emergencyCoolingSetpoint”, “decimal”, title: “Emergency Max Temp”, required: false
}
section(“The minimum difference between the heating and cooling setpoint, it’s recommended to not put this too low to conserve energy”) {
input “heatCoolDelta”, “decimal”, title: “Heat / Cool Delta”, defaultValue: 3.0
}
section(“The amount that the temperature is allowed to dip below the heating setpoint before engaging heating, it’s recommended to not put this too low to avoid heaters turning on and off too frequently”) {
input “heatDiff”, “decimal”, title: “Heat Differential”, defaultValue: 0.3
}
section(“The amount that the temperature is allowed to go above the cooling setpoint before engaging cooling, it’s recommended to not put this too low to avoid coolers turning on and off too frequently”) {
input “coolDiff”, “decimal”, title: “Cool Differential”, defaultValue: 0.3
}

section("Fix for unreliable switches to automatically turn them on/off again, if it seems like turning them on/off did not work based on the temperature (Experimental)") {
    input "unreliableSwitchFix", "bool", title: "Unreliable switch fix", defaultValue: false
}

}

def installed()
{
log.debug “running installed”
state.deviceID = Math.abs(new Random().nextInt() % 9999) + 1
updated()
}

def createDevice() {
def thermostat
def label = app.getLabel()

log.debug "create device with id: pmvt$state.deviceID, named: $label" //, hub: $sensor.hub.id"
try {
    thermostat = addChildDevice("piratemedia/smartthings", "Virtual Thermostat Device", "pmvt" + state.deviceID, null, [label: label, name: label, completedSetup: true])
} catch(e) {
    log.error("caught exception", e)
}
return thermostat

}

def motionDetected(){
if(motion) {
for(m in motion) {
if(m.currentValue(‘contact’) == “open”) {
return true;
}
}
}
return false;
}

def shouldHeatingBeOn(thermostat) {
def temp = getAverageTemperature()

//if temperature is below emergency setpoint
if(emergencyHeatingSetpoint && emergencyHeatingSetpoint > temp) {
	return true;
}

//if thermostat isn't set to heat
if(thermostat.currentValue('thermostatMode') != "heat" && thermostat.currentValue('thermostatMode') != "auto") {
	return false;
}

//if any of the contact sensors are open
if(motionDetected()){
    return false;
}

//average temperature across all temperature sensors is above set point
if(temp > thermostat.currentValue("adjustedHeatingPoint")) {
    return false;
}

return true;

}

def shouldCoolingBeOn(thermostat) {
def temp = getAverageTemperature()

//if temperature is above emergency setpoint
if(emergencyCoolingSetpoint && emergencyCoolingSetpoint < temp) {
    return true;
}

//if thermostat isn't set to cool
if(thermostat.currentValue('thermostatMode') != "cool" && thermostat.currentValue('thermostatMode') != "auto") {
    return false;
}

//if any of the contact sensors are open
if(motionDetected()){
    return false;
}

//average temperature across all temperature sensors is below set point
if(temp < thermostat.currentValue("adjustedCoolingPoint")) {
    return false;
}

return true;    

}

def getAverageTemperature() {
def total = 0;
def count = 0;

//total all sensors temperature
for(sensor in sensors) {
	total += sensor.currentValue("temperature")
    thermostat.setIndividualTemperature(sensor.currentValue("temperature"), count, sensor.label)
    count++
}

//divide by number of sensors
return total / count

}

def switchOff(switches) {
log.debug "switching off: {switches}, current values: " + switches.currentValue("switch") for(s in switches) { s.off() } log.debug "done switching off: {switches}, current values: " + switches.currentValue(“switch”)
}

def switchOn(switches) {
log.debug "switching on: {switches}, current values: " + switches.currentValue("switch") for(s in switches) { s.on() } log.debug "done switching on: {switches}, current values: " + switches.currentValue(“switch”)
}

//set the expected direction (heat/cool/none) to be able to monitor if it’s working
def setExpectedDirection(direction) {
log.debug “direction change to ${direction}”
state.expectedDirection = direction
state.directionChangeWorked = false
state.directionChangeTime = new Date().getTime()
}

def temperatureHandler(evt) {
state.curTemp = getAverageTemperature()
def now = new Date().getTime()
def minSinceDirectionChange = (now - state.directionChangeTime)/(1000*60)
log.debug “temperatureHandler: {evt.stringValue}, curTemp: {state.curTemp}, lastTemp: {state.lastTemp}" + ", expectedDirection: {state.expectedDirection}, directionChangeWorked: {state.directionChangeWorked}" + ", minSinceDirectionChange: {minSinceDirectionChange}, now: {now}, directionChangeTime: {state.directionChangeTime}”

if(!state.directionChangeWorked && state.expectedDirection != 'none') {
    //if we haven't proven that the direction change has worked yet, let's confirm that it worked
    if(state.expectedDirection == 'cool' && state.curTemp < state.lastTemp) {
        log.debug "expecting cool and temp went down from ${state.lastTemp} to ${state.curTemp} all good"
        state.directionChangeWorked = true
    }
    if(state.expectedDirection == 'heat' && state.curTemp > state.lastTemp) {
        log.debug "expecting heat and temp went up from ${state.lastTemp} to ${state.curTemp} all good"
        state.directionChangeWorked = true
    }

    if(!state.directionChangeWorked && minSinceDirectionChange > 4){
        if(!unreliableSwitchFix) {
            log.debug "direction change did not work within 4 min, but since 'Unreliable Switch Fix' is off, nothing will be done. Minutes since direction change: ${minSinceDirectionChange}"
            return
        }
        log.debug "direction change did not work within 4 min, try flipping the switch again and reset the timer. Minutes since direction change: ${minSinceDirectionChange}"
        state.directionChangeTime = new Date().getTime()
        def oState = thermostat.getOperatingState()
        if(state.expectedDirection == 'cool') {
            if(oState == 'cooling') {
                switchOn(cooling_outlets)
            } else {
                switchOff(heating_outlets)
            }
        }
        if(state.expectedDirection == 'heat') {
            if(oState == 'heating') {
                switchOn(heating_outlets)
            } else {
                switchOff(cooling_outlets)
            }
        }
    }
}

state.lastTemp = state.curTemp
handleChange()

}

def cool() {
//log.debug "cooling outlets on, current value: " + cooling_outlets.currentValue(“switch”)
def oState = thermostat.getOperatingState()
log.debug “In cool(). oState=” + oState;
if(oState != ‘cooling’) {
setExpectedDirection(‘cool’)
thermostat.setThermostatOperatingState(‘cooling’)
switchOn(cooling_outlets)
if(oState == ‘heating’) {
switchOff(heating_outlets)
}
}
else {
log.debug “About to call setcoolfanspeed()”;
setcoolfanspeed(cooling_outlets)
}
}

def heat() {
//log.debug "heating outlets on, current value: " + heating_outlets.currentValue(“switch”)
def oState = thermostat.getOperatingState()
if(oState != ‘heating’) {
setExpectedDirection(‘heat’)
thermostat.setThermostatOperatingState(‘heating’)
switchOn(heating_outlets)
if(oState == ‘cooling’) {
switchOff(cooling_outlets)
}
}
}

def off() {
//log.debug "off, all outlets off, current value heating: " + heating_outlets.currentValue(“switch”) + ", cooling: " + cooling_outlets.currentValue(“switch”)
def oState = thermostat.getOperatingState()
if(oState != ‘off’) {
thermostat.setThermostatOperatingState(‘off’)
setExpectedDirection(‘none’)
if(oState == ‘heating’) {
switchOff(heating_outlets)
} else if(oState == ‘cooling’) {
switchOff(cooling_outlets)
}
}
}

def idle() {
//log.debug "idle, all outlets off, current value heating: " + heating_outlets.currentValue(“switch”) + ", cooling: " + cooling_outlets.currentValue(“switch”)
def oState = thermostat.getOperatingState()
if(oState != ‘idle’) {
thermostat.setThermostatOperatingState(‘idle’)
if(oState == ‘heating’) {
setExpectedDirection(‘cool’)
switchOff(heating_outlets)
} else if(oState == ‘cooling’) {
setExpectedDirection(‘heat’)
switchOff(cooling_outlets)
}
}
}

def handleChange() {
def thermostat = getThermostat()
if(thermostat) {
log.debug "handle change, mode: " + thermostat.currentValue(‘thermostatMode’) +
", operatingState: " + thermostat.currentValue(“thermostatOperatingState”) +
", temp: " + getAverageTemperature() +
", coolingSetPoint: " + thermostat.currentValue(“coolingSetpoint”) +
", heatingSetPoint: " + thermostat.currentValue(“heatingSetpoint”)

    /*def attrs = thermostat.supportedAttributes
	attrs.each {
	  log.debug "${thermostat.displayName}, attribute: ${it.name}, dataType: ${it.dataType}, value: " + thermostat.currentValue(it.name)
	}*/

	switch (thermostat.currentValue('thermostatMode')){
        case "heat":
            if(shouldHeatingBeOn(thermostat)) {
                heat()
            } else {
                idle()
            }
            break
        case "cool":
            if(shouldCoolingBeOn(thermostat)) {
                cool()
            } else {
                idle()
            }
            break
        case "auto":
            if(shouldCoolingBeOn(thermostat)) {
                cool()
            } else if(shouldHeatingBeOn(thermostat)) {
                heat()
            } else {
                idle()
            }
            break
        case "off":
        default:
            off()
            break
    }
    getThermostat().setVirtualTemperature(getAverageTemperature())
}

}

def getThermostat() {
return getChildDevice(“pmvt” + state.deviceID)
}

def uninstalled() {
deleteChildDevice(“pmvt” + state.deviceID)
}

def updated()
{
log.debug “running updated: $app.label”
unsubscribe()
unschedule()

//get or add thermostat
def thermostat = getThermostat()
if(thermostat == null) {
    thermostat = createDevice()
}

//subscribe to temperature changes
subscribe(sensors, "temperature", temperatureHandler)

//subscribe to contact sensor changes
if (motion) {
	subscribe(motion, "contact", motionHandler)
}

//subscribe to virtual device changes
//subscribe(thermostat, "thermostatSetpoint", thermostatSetPointHandler)
subscribe(thermostat, "heatingSetpoint", heatingSetPointHandler)
subscribe(thermostat, "coolingSetpoint", coolingSetPointHandler)
subscribe(thermostat, "thermostatMode", thermostatModeHandler)

//reset some values
setExpectedDirection('none')
thermostat.clearSensorData()
thermostat.setVirtualTemperature(getAverageTemperature())
thermostat.setHeatCoolDelta(heatCoolDelta)
thermostat.setHeatDiff(heatDiff)
thermostat.setCoolDiff(coolDiff)

}

def coolingSetPointHandler(evt) {
log.debug “coolingSetPointHandler: ${evt.stringValue}”
handleChange()
}

def heatingSetPointHandler(evt) {
log.debug “heatingSetPointHandler: ${evt.stringValue}”
handleChange()
}

def motionHandler(evt) {
log.debug “motionHandler: ${evt.stringValue}”
handleChange()
}

def thermostatModeHandler(evt) {
log.debug “thermostatModeHandler: ${evt.stringValue}”
handleChange()
}

def setcoolfanspeed(switches) {
def temp = getAverageTemperature()
def thermostat = getThermostat()
def speed = 0

log.debug "setting fan speed on: ${switches}, current values: " + switches.currentValue("level") + " coolDiff= " + coolDiff;

//Determine Desired Fan Speed
if(temp >= thermostat.currentValue("coolingSetpoint") + (coolDiff)*3) {
    speed=75;
}
else { 
    if(temp >= thermostat.currentValue("coolingSetpoint") + (coolDiff)*2) {
        speed=50;
    }
    else {
        speed=25;
    }
}

//Set the fan speed on each switch
for(s in switches) {
	log.debug "temp = " + temp + "; thermostat.currentValue#coolingSetpoint#)= " + thermostat.currentValue("coolingSetpoint") + "; thermostat.coolDiff= " + 
		coolDiff
    log.debug "Setting Level on: " + s + ", to: " + speed;
    s.setLevel(speed);
}
log.debug "done setting fan speed: ${switches}, speed: " + speed

}

To me it looks like this will only work if all your cool switches has a fan speed, which they may not in all cases (mine doesn’t). Also, as far as I can see this would only set the fan speed the second time the cool() method is called as it’s in the else section.

But I like the idea of doing this and being able to set the intensity of the fan based on how far the temperature is from the desired temperature, should probably just do it with a flag, so that people can turn on or off this feature depending on whether their cooling switches has a fan speed and I would imagine that it should be possible to support the same for heating switches, so would need two flags. A different idea would be to have two set of cooling and heating switches, some that are on/off and some that has a level, then people can add their switches to the desired sections.

1 Like

Good morning, I am interested in this DTH, I have a virtual air conditioner via tasmota with a tuya ir bridge, which I want to put as a thermostat on the homebridge.
Can you send to me the code ? or put on github ?

Thanks @Steffen_Nissen. I have made the suggested change in my code base.
Do you know how I can query whether a device has a level?