HRT4-ZW or SRT321 Thermostat driver?

Has anyone got a Horstmann HRT-ZW (aka secure SRT321) thermostat working on Hubitat, and if so which driver are you using? I'm just starting to look at moving my heating control over to Hubitat and I have one of these in my drawer of many gadgets.

You can try the generic zwave thermostat driver, no idea if it will work with this device or not, let us know whay you find out.

Will do, I was just hoping someone else was the guinea pig, but it looks like it's me! I'll give it a try tonight.

After my first attempts tonight to set this up, I remembered why it was in my drawer of many gadgets rather than actually in use! It's not an intuitive device to set up to say the least. But I did manage to get it talking to HE. It wasn't recognised in discover device, but it appeared in the Zwave list and I was able to define it from there as Generic Zwave thermostat. It picked up battery level, internal temperature, and heating setpoint. If I adjusted the setpoint on the stat it was updated after a while on HE, but I could not get HE to send a new setpoint to the stat. Also, by connecting to HE it lost its connection to the ASR-ZW receiver/relay, so it wouldn't actually switch anything on or off anyway. I started reading up about it all again but just got more and more confused, so have given up for now. To be continued......

So I found a ST driver for this device and have copied it and converted it for HE by simply changing all physicalgraph instances to hubitat. This is it as I have it loaded now below:

It works up to a point. When I adjust the set temp on the stat, the heatingSetpoint is updated instantly. HE reads the internal temp of the stat fine too and seems to update when the temp has changed by a degree C. The problem is that nothing that I set via the Device page in HE gets across to the stat itself. So if I change the Set Temp either by putting a direct value in, or by using the Set Temp Up or Set Temp Down buttons, the value changes fine on the Current States list for the device but does not change on the stat. Obviously to have a useful stat it needs to be able to have its target temp updated by HE, so I need to do some more research. I presume that it must have updated OK in ST so maybe my basic conversion from ST to HE needs some work.

/*
 * SRT321 Thermostat by MeavyDev
 * With support for an associated switch set by the SRT321 helper app
 */
metadata 
{
	definition (name: "SRT321 Thermostat", namespace: "meavydev", author: "MeavyDev") 
    {
    	capability "Actuator"
		capability "Temperature Measurement"
		capability "Thermostat"
		capability "Configuration"
		capability "Polling"
		capability "Sensor"

		command "switchMode"
        command "quickSetHeat"
		command "setTemperature"
		command "setTempUp"
		command "setTempDown"
        
		command "setupDevice" 
        		
		// fingerprint deviceId: "0x0800", inClusters: "0x25, 0x31, 0x40, 0x43, 0x70, 0x72, 0x80, 0x84, 0x85, 0x86, 0xef"
		fingerprint deviceId: "0x0800" 
        fingerprint inClusters: "0x72,0x86,0x80,0x84,0x31,0x43,0x85,0x70,0x40,0x25"
	}

	// simulator metadata
	simulator 
    {
	}

	tiles (scale: 2)
    {
        multiAttributeTile(name:"heatingSetpoint", type: "thermostat", width: 6, height: 4, canChangeIcon: true)
        {
            tileAttribute ("device.heatingSetpoint", key: "PRIMARY_CONTROL") 
            {
                attributeState("default", unit:"dC", label:'${currentValue}°')
            }
            
            tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") 
            {
                attributeState("VALUE_UP", action: "setTempUp")
                attributeState("VALUE_DOWN", action: "setTempDown")
//                attributeState("default", action: "setTemperature")
            }
            
            tileAttribute("device.temperature", key: "SECONDARY_CONTROL") 
            {
            	attributeState("default", label:'${currentValue} C', unit:"C")
            }
            
            tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") 
            {
                attributeState("off", label:'${name}')
                attributeState("heat", label:'${name}')
            }
            
  			tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") 
            {
    			attributeState("default", label:'${currentValue}', unit:"C")
			}
            
            tileAttribute("device.thermostatMode", key: "OPERATING_STATE") 
            {
                attributeState("off", backgroundColor:"#44b621")
                attributeState("heat", backgroundColor:"#ffa81e")
            }
        }  

        valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) 
        {
            tileAttribute ("device.battery", key: "PRIMARY_CONTROL")
            {
                state "battery", label:'${currentValue}% battery', unit:""
            }
        }
        
        standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat", width: 2, height: 2)
        {
			state "default", action:"polling.poll", icon:"st.secondary.refresh"
		}
        
		standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) 
        {
			state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
		}
	}

	main "heatingSetpoint"
    details(["heatingSetpoint", "battery", "refresh", "configure", "temperature", "mode"])

    preferences 
    {
        input "userWakeUpInterval", "number", title: "Wakeup interval...", description: "Wake Up Interval (seconds)", defaultValue: 3600, required: false, displayDuringSetup: false

        // This is the "Device Network Id" displayed in the IDE
        input "userAssociatedDevice", "string", title:"Associated z-wave switch network Id...", description:"Associated switch ZWave network Id (hex)", required: false, displayDuringSetup: false
    }
}

def parse(String description)
{
//	log.debug "Parse $description"

	def result = zwaveEvent(zwave.parse(description, [0x72:1, 0x86:1, 0x80:1, 0x84:2, 0x31:1, 0x43:1, 0x85:1, 0x70:1, 0xEF:1, 0x40:1, 0x25:1]))
	if (!result) 
    {
    	log.warn "Parse returned null"
		return null
	}
    
//	log.debug "Parse returned $result"
	result
}

def updated() 
{
	log.debug "preferences updated"

	state.configNeeded = true
}

def zwaveEvent(hubitat.zwave.commands.thermostatmodev1.ThermostatModeSet cmd)
{
	def map = [:]
	switch (cmd.mode) {
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeSet.MODE_OFF:
			map.value = "off"
			break
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeSet.MODE_HEAT:
			map.value = "heat"
	}
	map.name = "thermostatMode"
	createEvent(map)
}

// Event Generation
def zwaveEvent(hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointReport cmd)
{
	def map = [:]
	map.value = cmd.scaledValue.toString()
	map.unit = cmd.scale == 1 ? "F" : "C"
	map.displayed = false
	switch (cmd.setpointType) {
		case 1:
			map.name = "heatingSetpoint"
			break;
		default:
			return [:]
	}
	// So we can respond with same format
	state.size = cmd.size
	state.scale = cmd.scale
	state.precision = cmd.precision
	createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.sensormultilevelv1.SensorMultilevelReport cmd)
{
	def map = [:]
	map.value = cmd.scaledSensorValue.toString()
	map.unit = cmd.scale == 1 ? "F" : "C"
	map.name = "temperature"
	createEvent(map)
}

//OLD code - removed by PH
/*
// Battery powered devices can be configured to periodically wake up and
// check in. They send this command and stay awake long enough to receive
// commands, or until they get a WakeUpNoMoreInformation command that
// instructs them that there are no more commands to receive and they can
// stop listening.
def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd)
{
		def map = [name:"thermostatWakeUp", value: "${device.displayName} woke up", isStateChange: true]       
        def event = createEvent(map)
		def cmds = updateIfNeeded()
        
		cmds << zwave.wakeUpV2.wakeUpNoMoreInformation().format()
        
        log.debug "Wakeup $cmds"

        [event, response(delayBetween(cmds, 1000))]      
}
*/

// New code added by PH

// Battery powered devices can be configured to periodically wake up and
// check in. They send this command and stay awake long enough to receive
// commands, or until they get a WakeUpNoMoreInformation command that
// instructs them that there are no more commands to receive and they can
// stop listening.
def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd)
{
def map = [name:"thermostatWakeUp", value: "${device.displayName} woke up", isStateChange: true] 
def event = createEvent(map)
def cmds = updateIfNeeded()

	cmds << zwave.wakeUpV2.wakeUpNoMoreInformation().format()

    log.debug "Wakeup $cmds"

	cmds.each 
    { zwaveCmd ->
            def hubCmd = []
            hubCmd << response(zwaveCmd)

            log.debug "HubCmds $hubCmd"

            sendHubCommand(hubCmd, 1000)

            log.debug "Sent hubcommand"
    };
    [event]      
    
    // [event, response(delayBetween(cmds, 1000))]
}


def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) 
{
    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
            log.debug ("Battery: $cmd.batteryLevel")
    }
    // Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
    state.lastbatt = new Date().time
    createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.thermostatmodev1.ThermostatModeReport cmd) 
{
	def map = [:]
	switch (cmd.mode) {
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeReport.MODE_OFF:
			map.value = "off"
			break
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeReport.MODE_HEAT:
			map.value = "heat"
			break
	}
	map.name = "thermostatMode"
	createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpIntervalCapabilitiesReport cmd) 
{
    def map = [ name: "defaultWakeUpInterval", unit: "seconds" ]
	map.value = cmd.defaultWakeUpIntervalSeconds
	map.displayed = false
	state.defaultWakeUpInterval = cmd.defaultWakeUpIntervalSeconds
    createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpIntervalReport cmd)
{
	def map = [ name: "reportedWakeUpInterval", unit: "seconds" ]
	map.value = cmd.seconds
	map.displayed = false
    createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) 
{
	log.debug "Zwave event received: $cmd"
}

def zwaveEvent(hubitat.zwave.Command cmd) 
{
	log.warn "Unexpected zwave command $cmd"
    
    delayBetween([
		zwave.sensorMultilevelV1.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
		zwave.thermostatModeV1.thermostatModeGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
	], 1000)

}

// Command Implementations

def configure() 
{
	log.debug "configure"
	state.configNeeded = true
    
    // Normally this won't do anything as the thermostat is asleep, 
    // but do this in case it helps with the initial config
	delayBetween([
		zwave.thermostatModeV1.thermostatModeSupportedGet().format(),
		zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
        // Set hub to get battery reports / warnings
        zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format(),
        // Set hub to get set point reports
        zwave.associationV1.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]).format(),
        // Set hub to get multi-level sensor reports (defaults to temperature changes of > 1C)
        zwave.associationV1.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format(),
        // set the temperature sensor On
		zwave.configurationV1.configurationSet(configurationValue: [0xff], parameterNumber: 1, size: 1).format()
	], 1000)
}

def poll() 
{
	log.debug "poll"

	// Normally this won't do anything as the thermostat is asleep, 
    // but do this in case it helps with the initial config
	delayBetween([
		zwave.sensorMultilevelV1.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1).format(),
		zwave.thermostatModeV1.thermostatModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
	], 1000)
}

def refresh()
{
	log.debug "refresh"

	state.refreshNeeded = true
    
    // Normally this won't do anything as the thermostat is asleep, 
    // but do this in case it helps with the initial config
	delayBetween([
		zwave.sensorMultilevelV1.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1).format(),
		zwave.thermostatModeV1.thermostatModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
	], 1000)
}

def quickSetHeat(degrees) 
{
	setHeatingSetpoint(degrees)
	log.debug("Degrees at quicksetheat: $degrees")
}

def setTempUp() 
{ 
    def newtemp = device.currentValue("heatingSetpoint").toInteger() + 1
    log.debug "Setting temp up: $newtemp"
    quickSetHeat(newtemp)
}

def setTempDown() 
{ 
    def newtemp = device.currentValue("heatingSetpoint").toInteger() - 1
    log.debug "Setting temp down: $newtemp"
    quickSetHeat(newtemp)
}

def setTemperature(temp)
{
	log.debug "setTemperature $temp"
    quickSetHeat(temp)
}

def setHeatingSetpoint(degrees) 
{
	setHeatingSetpoint(degrees.toDouble())
	log.debug("Degrees at setheatpoint: $degrees")
}

def setHeatingSetpoint(Double degrees) 
{
	log.trace "setHeatingSetpoint($degrees)"
    sendEvent(name: 'heatingSetpoint', value: degrees)

	def deviceScale = state.scale ?: 1
	def deviceScaleString = deviceScale == 2 ? "C" : "F"
    def locationScale = getTemperatureScale()
    def p = (state.precision == null) ? 1 : state.precision

    def convertedDegrees
    if (locationScale == "C" && deviceScaleString == "F") 
    {
        convertedDegrees = celsiusToFahrenheit(degrees)
    } 
    else if (locationScale == "F" && deviceScaleString == "C") 
    {
        convertedDegrees = fahrenheitToCelsius(degrees)
    } 
    else 
    {
        convertedDegrees = degrees
    }

	log.trace "setHeatingSetpoint scale: $deviceScale precision: $p setpoint: $convertedDegrees"
	state.deviceScale = deviceScale
    state.p = p
    state.convertedDegrees = convertedDegrees
    state.updateNeeded = true
    
    thermostatMode
}

private getStandardDelay() 
{
	1000
}

def updateIfNeeded()
{
	def cmds = []
    
    // Only ask for battery if we haven't had a BatteryReport in a while
    if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) 
    {
    	log.debug "Getting battery state"
    	cmds << zwave.batteryV1.batteryGet().format()
    }
        
	if (state.refreshNeeded)
    {
        log.debug "Refresh"
        cmds << zwave.sensorMultilevelV1.sensorMultilevelGet().format() // current temperature
		cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1).format()

		cmds << zwave.thermostatModeV1.thermostatModeGet().format()
		cmds << zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
        cmds << zwave.thermostatModeV1.thermostatModeSupportedGet().format()
       	state.refreshNeeded = false
    }
    
    if (state.updateNeeded)
    {
        log.debug "Update"

		cmds << zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1, scale: state.deviceScale, precision: state.p, scaledValue: state.convertedDegrees).format()
        state.updateNeeded = false
    }
    
    if (state.configNeeded)
    {
        log.debug "Config"
    	state.configNeeded = false
        
        // Nodes controlled by Thermostat Mode Set - not sure this is needed?
        cmds << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()
        
         // Set hub to get battery reports / warnings
        cmds << zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
        
         // Set hub to get set point reports
        cmds << zwave.associationV1.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]).format()
        
         // Set hub to get multi-level sensor reports (defaults to temperature changes of > 1C)
        cmds << zwave.associationV1.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format()
        
        // set the temperature sensor On
		cmds << zwave.configurationV1.configurationSet(configurationValue: [0xff], parameterNumber: 1, size: 1).format()

		log.debug "association $state.association user: $userAssociatedDevice"
		int nodeID = getAssociatedId(state.association)
        // If user has changed the switch association, send the new assocation to the device 
    	if (nodeID != -1)
        {
            log.debug "Setting associated device $nodeID"
            cmds << zwave.associationV1.associationSet(groupingIdentifier: 2, nodeId: nodeID).format()
        }
        
        def userWake = getUserWakeUp(userWakeUpInterval)
        // If user has changed userWakeUpInterval, send the new interval to the device 
    	if (state.wakeUpInterval != userWake)
        {
       		state.wakeUpInterval = userWake
            log.debug "Setting New WakeUp Interval to: " + state.wakeUpInterval
        	cmds << zwave.wakeUpV2.wakeUpIntervalSet(seconds:state.wakeUpInterval, nodeid:zwaveHubNodeId).format()
       		cmds << zwave.wakeUpV2.wakeUpIntervalGet().format()
    	}  
		cmds << zwave.thermostatModeV1.thermostatModeSupportedGet().format()
    }
    
    if (cmds.size() > 0)
    {
    	cmds << "delay 2000"
    }
    cmds
}


private getUserWakeUp(userWake) 
{
    if (!userWake)  
    { 
    	userWake = '3600' // set default 1 hr if no user preference 
    } 
    // make sure user setting is within valid range for device 
    if (userWake.toInteger() < 60)
    { 
    	userWake = '600'   // 10 minutes - Mininum
    }
    if (userWake.toInteger() > 36000)
    {
    	userWake = '36000' // 10 hours - Maximum
    }  
    return userWake.toInteger()
}

// Get the Z-Wave Id of the binary switch the user wants the thermostat to control
// -1 if no association set
int getAssociatedId(association)
{
	int associatedState = -1
	int associatedUser = -1
    log.debug "getAssociatedId $association"
	if (association != null && association != "")
    {
    	associatedState = association.toInteger()
        log.debug "State $association $associatedState"
    }
    if (userAssociatedDevice != null && userAssociatedDevice != "")
    {
    	try
        {
       		associatedUser = Integer.parseInt(userAssociatedDevice, 16)
        }
        catch (Exception e)
        {
        }
        log.debug "userDev $userAssociatedDevice $associatedUser"
    }
    
    // Use the app associated switch id if it exists, otherwise the device preference  
    return associatedState != -1 ? associatedState : associatedUser
}

// Called from the SRT321 App with the Z-Wave switch network ID
// How long before SmartThings realises that having device preferences 
// with input "*" "capability.switch" is reasonable????
void setupDevice(value)
{
	state.association = "$value"
    int val = Integer.parseInt(value)
    String hex = Integer.toHexString(val)
    log.debug "Setting associated device Id $value Hex $hex"
    settings.userAssociatedDevice = hex
    state.configNeeded = true
}

Just a friendly tip... When copying and pasting code, please highlight all of the code, then click the Preformatted Text button (looks like </>) in the menu. This will make your code much more readable for others, and will properly preserve the characters that sometimes get mangled otherwise.

1 Like

Thanks for the tip, I didn't know that. I have edited my post now so hopefully it's a bit clearer. Thanks again.

1 Like

Hi,
Did you ever get this to work.

No. I think it was more to do with restrictions in the SRT321 itself rather than the driver, but I gave up in the end.

What Thermostat set up did you go for in the end?

I have no physical thermostat at all. I use a virtual thermostat in Hubitat that takes temperature values from sensors using Andy's (@Cobra) average temperature app. I then control my heating with a zwave switch connected to the boiler. A simple RM rule (if stat mode is heating then boiler switch on, else boiler switch off) ensures the boiler operation mirrors the thermostat mode. I've been running it in various forms for several months and it works really well. I have an android tablet permanently displaying my heating dashboard page as a user interface, but in reality it looks after itself and that is more a visual check for me that everything is working ok.

The SRT321 works fine with hubitat. You can change the heatingsetpoint and it will update back onto the device - but only after the device checks-in with the hub - by default that's every 3600 seconds (1 hour)….. I have zoned my house and I have one of these in each zone, so that the set temperature for the zone can be dialled in by any non-techie user. here's my converted DH.

    /*
 * SRT321 Thermostat by MeavyDev - Converted for Hubitat by Scruffy-SJB
 */

metadata 
{
	definition (name: "SRT321 Thermostat", namespace: "meavydev", author: "MeavyDev & Scruffy-SJB",
                mnmn: "SmartThings", vid: "generic-thermostat", ocfDeviceType: "oic.d.thermostat") 
    {
    	capability "Actuator"
		capability "Temperature Measurement"
		capability "Thermostat"
		capability "Switch"
		capability "Configuration"
		capability "Polling"
		capability "Sensor"
        capability "Health Check"
        capability "Battery"
        
		command "switchMode"
        command "quickSetHeat"
		command "setTemperature"
		command "setTempUp"
		command "setTempDown"
 		command "configure"
		command "poll"
		command "refresh"
		
		fingerprint deviceId: "0x0800" 
        fingerprint inClusters: "0x72,0x86,0x80,0x84,0x31,0x43,0x85,0x70,0x40,0x25"
	}

	main "heatingSetpoint"
    details(["heatingSetpoint", "battery", "refresh", "configure", "temperature", "mode"])

    preferences 
    {
        input "userWakeUpInterval", "number", title: "Wakeup interval...", description: "Wake Up Interval (seconds)", defaultValue: 900, required: false, displayDuringSetup: false
    }
}

def parse(String description)
{
//	log.debug "Parse $description"

	def result = zwaveEvent(zwave.parse(description, [0x72:1, 0x86:1, 0x80:1, 0x84:2, 0x31:1, 0x43:1, 0x85:1, 0x70:1, 0xEF:1, 0x40:1, 0x25:1]))
	if (!result) 
    {
    	log.warn "Parse returned null"
		return null
	}
    
//	log.debug "Parse returned $result"
	result
}

def installed() 
{
	log.debug "preferences installed"

	state.configNeeded = true
    sendHealthCheckInterval()
}


def updated() 
{
	log.debug "preferences updated"

	state.configNeeded = true
    sendHealthCheckInterval()
}

def sendHealthCheckInterval()
{
    // Device-Watch simply pings if no device events received for checkInterval
	sendEvent(name: "checkInterval", value: 15 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
}

def ping()
{
	log.debug "ping"
    state.refreshNeeded = true
}

def zwaveEvent(hubitat.zwave.commands.thermostatmodev1.ThermostatModeSet cmd)
{
	def map = [:]
	switch (cmd.mode) {
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeSet.MODE_OFF:
			map.value = "off"
			break
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeSet.MODE_HEAT:
			map.value = "heat"
	}
	map.name = "thermostatMode"
	createEvent(map)
}

// Event Generation
def zwaveEvent(hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointReport cmd)
{
	def map = [:]
	map.value = cmd.scaledValue.toString()
	map.unit = cmd.scale == 1 ? "F" : "C"
	map.displayed = false
	switch (cmd.setpointType) {
		case 1:
			map.name = "heatingSetpoint"
			break;
		default:
			return [:]
	}
	// So we can respond with same format
	state.size = cmd.size
	state.scale = cmd.scale
	state.precision = cmd.precision
	createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.sensormultilevelv1.SensorMultilevelReport cmd)
{
	def map = [:]
	map.value = cmd.scaledSensorValue.toString()
	map.unit = cmd.scale == 1 ? "F" : "C"
	map.name = "temperature"
	createEvent(map)
}

// Battery powered devices can be configured to periodically wake up and
// check in. They send this command and stay awake long enough to receive
// commands, or until they get a WakeUpNoMoreInformation command that
// instructs them that there are no more commands to receive and they can
// stop listening.

def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd)
{
		def map = [name:"thermostatWakeUp", value: "${device.displayName} woke up", isStateChange: true]       
        def event = createEvent(map)
		def cmds = updateIfNeeded()
        
		cmds << zwave.wakeUpV2.wakeUpNoMoreInformation().format()
        
        log.debug "Wakeup $cmds"

        [event, response(delayBetween(cmds, 1000))]
}

def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) 
{
    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
            log.debug ("Battery: $cmd.batteryLevel")
    }
    // Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
    state.lastbatt = new Date().time
    createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.thermostatmodev1.ThermostatModeReport cmd) 
{
	def map = [:]
	switch (cmd.mode) {
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeReport.MODE_OFF:
			map.value = "off"
			break
		case hubitat.zwave.commands.thermostatmodev1.ThermostatModeReport.MODE_HEAT:
			map.value = "heat"
			break
	}
	map.name = "thermostatMode"
	createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpIntervalCapabilitiesReport cmd) 
{
    def map = [ name: "defaultWakeUpInterval", unit: "seconds" ]
	map.value = cmd.defaultWakeUpIntervalSeconds
	map.displayed = false
	state.defaultWakeUpInterval = cmd.defaultWakeUpIntervalSeconds
    createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpIntervalReport cmd)
{
	def map = [ name: "reportedWakeUpInterval", unit: "seconds" ]
	map.value = cmd.seconds
	map.displayed = false
    createEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) 
{
	log.debug "Zwave event received: $cmd"
}

def zwaveEvent(hubitat.zwave.Command cmd) 
{
	log.warn "Unexpected zwave command $cmd"
    
    delayBetween([
		zwave.sensorMultilevelV1.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
		zwave.thermostatModeV1.thermostatModeGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
	], 1000)

}

// Command Implementations

def configure() 
{
	log.debug "configure"
	state.configNeeded = true
    
    // Normally this won't do anything as the thermostat is asleep, 
    // but do this in case it helps with the initial config
	delayBetween([
		zwave.thermostatModeV1.thermostatModeSupportedGet().format(),
		zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
        // Set hub to get battery reports / warnings
        zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format(),
        // Set hub to get set point reports
        zwave.associationV1.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]).format(),
        // Set hub to get multi-level sensor reports (defaults to temperature changes of > 1C)
        zwave.associationV1.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format(),
        // set the temperature sensor On
		zwave.configurationV1.configurationSet(configurationValue: [0xff], parameterNumber: 1, size: 1).format()
	], 1000)
}

def poll() 
{
	log.debug "poll"

	// Normally this won't do anything as the thermostat is asleep, 
    // but do this in case it helps with the initial config
	delayBetween([
		zwave.sensorMultilevelV1.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1).format(),
		zwave.thermostatModeV1.thermostatModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
	], 1000)
}

def refresh()
{
	log.debug "refresh"

	state.refreshNeeded = true
    
    // Normally this won't do anything as the thermostat is asleep, 
    // but do this in case it helps with the initial config
	delayBetween([
		zwave.sensorMultilevelV1.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1).format(),
		zwave.thermostatModeV1.thermostatModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
	], 1000)
}

def quickSetHeat(degrees) 
{
	setHeatingSetpoint(degrees)
	log.debug("Degrees at quicksetheat: $degrees")
}

def setTempUp() 
{ 
    def newtemp = device.currentValue("heatingSetpoint").toInteger() + 1
    log.debug "Setting temp up: $newtemp"
    quickSetHeat(newtemp)
}

def setTempDown() 
{ 
    def newtemp = device.currentValue("heatingSetpoint").toInteger() - 1
    log.debug "Setting temp down: $newtemp"
    quickSetHeat(newtemp)
}

def setTemperature(temp)
{
	log.debug "setTemperature $temp"
    quickSetHeat(temp)
}

def setHeatingSetpoint(degrees) 
{
	setHeatingSetpoint(degrees.toDouble())
	log.debug("Degrees at setheatpoint: $degrees")
}

def setHeatingSetpoint(Double degrees) 
{
	log.trace "setHeatingSetpoint($degrees)"
    sendEvent(name: 'heatingSetpoint', value: degrees)

	def deviceScale = state.scale ?: 1
	def deviceScaleString = deviceScale == 2 ? "C" : "F"
    def locationScale = getTemperatureScale()
    def p = (state.precision == null) ? 1 : state.precision

    def convertedDegrees
    if (locationScale == "C" && deviceScaleString == "F") 
    {
        convertedDegrees = celsiusToFahrenheit(degrees)
    } 
    else if (locationScale == "F" && deviceScaleString == "C") 
    {
        convertedDegrees = fahrenheitToCelsius(degrees)
    } 
    else 
    {
        convertedDegrees = degrees
    }

	log.trace "setHeatingSetpoint scale: $deviceScale precision: $p setpoint: $degrees"
	state.deviceScale = deviceScale
    state.p = p
    state.convertedDegrees = convertedDegrees
    state.updateNeeded = true
    // thermostatMode
}

private getStandardDelay() 
{
	1000
}

def updateIfNeeded()
{
	def cmds = []
    
    log.debug "updateIfNeeded"
    
    // Only ask for battery if we haven't had a BatteryReport in a while
    if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) 
    {
    	log.debug "Getting battery state"
    	cmds << zwave.batteryV1.batteryGet().format()
    }
        
	if (state.refreshNeeded)
    {
        log.debug "Refresh"
        sendEvent(name:"SRT321", value: "Refresh")

        cmds << zwave.sensorMultilevelV1.sensorMultilevelGet().format() // current temperature
		cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1).format()

		cmds << zwave.thermostatModeV1.thermostatModeGet().format()
		cmds << zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
        cmds << zwave.thermostatModeV1.thermostatModeSupportedGet().format()
       	state.refreshNeeded = false
    }
    
    if (state.updateNeeded)
    {
        log.debug "Updating setpoint $state.convertedDegrees"
		sendEvent(name:"SRT321", value: "Updating setpoint to $state.convertedDegrees")
		cmds << zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: hubitat.zwave.commands.thermostatsetpointv1.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1, scale: state.deviceScale, precision: state.p, scaledValue: state.convertedDegrees).format()
        state.updateNeeded = false
    }
    
    if (state.configNeeded)
    {
        log.debug "Config"
		sendEvent(name:"SRT321", value: "Config")
    	state.configNeeded = false
        
        // Nodes controlled by Thermostat Mode Set - not sure this is needed?
        cmds << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()
        
         // Set hub to get battery reports / warnings
        cmds << zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
        
         // Set hub to get set point reports
        cmds << zwave.associationV1.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]).format()
        
         // Set hub to get multi-level sensor reports (defaults to temperature changes of > 1C)
        cmds << zwave.associationV1.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format()
        
        // set the temperature sensor On
		cmds << zwave.configurationV1.configurationSet(configurationValue: [0xff], parameterNumber: 1, size: 1).format()
      
        def userWake = getUserWakeUp(userWakeUpInterval)
        // If user has changed userWakeUpInterval, send the new interval to the device 
    	if (state.wakeUpInterval != userWake)
        {
       		state.wakeUpInterval = userWake
            log.debug "Setting New WakeUp Interval to: " + state.wakeUpInterval
        	cmds << zwave.wakeUpV2.wakeUpIntervalSet(seconds:state.wakeUpInterval, nodeid:zwaveHubNodeId).format()
       		cmds << zwave.wakeUpV2.wakeUpIntervalGet().format()
    	}  
		cmds << zwave.thermostatModeV1.thermostatModeSupportedGet().format()
    }
    
    if (cmds.size() > 0)
    {
    	cmds << "delay 2000"
    }
    cmds
}

private getUserWakeUp(userWake) 
{
    if (!userWake)  
    { 
    	userWake = '900' // set default 15 mins if no user preference 
    } 
    // make sure user setting is within valid range for device 
    if (userWake.toInteger() < 60)
    { 
    	userWake = '600'   // 10 minutes - Mininum
    }
    if (userWake.toInteger() > 36000)
    {
    	userWake = '36000' // 10 hours - Maximum
    }  
    return userWake.toInteger()
}
2 Likes

Sorry to take heed of an old thread but I've got this set-up and thanks to your driver all works fine. I'm experiencing weird things on the dashboard.

When using the thermostat tile it gave me the temperature setting up and down arrows and all appeared to work fine. I added the scheduler to set different temperatures through the day and all of a sudden the tile displays "24C and unknown", unknown fan mode and mode off. There's no set point or scope for adjustment. Any suggestions?

It sounds like somehow the thermostat has been taken out of heating mode. The adjustment arrows in the thermostat tile only appear if the relevant mode is operational.

That is a great insight to why it has changed. I would think that the scheduler has done this. I shall check what the scheduler updates in terms of, i only thought it was heat adjusting. I'll delete the scheduler also and see if after the device updates that it changes back.

You shouldn't need to delete the scheduler. You can just put it into hold mode. Then you should be able to change the mode to heat (or auto) from either the thermostat tile or the thermostat device page

I've excluded, and included the device again to start fresh, deleted the scheduler and rules. After selecting the STR321 type hitting save and readding a tile for thermostat it remains the same.

23.2c and unknown
Unknown__ | Unknown
mode_____| fan mode

I've tried changing the "set thermostat mode" to heat however on pressing save it keeps resorting to auto

Also having come from a vera background there was a function to associate devices together so they were independent from needing to run with the controller thus saving processing and rules. Does Hubitat operate on this level at all or must the link be made in rules for a thermostat to boiler controller? Just seems a bit excessive to set 2 rules for a room stat?!

Ok I've just found out what is happening. The dashboard functions perfectly fine if you set the set point above ambient i.e. calling for heat but must be done from the room stat physically. If you then turn set point to below the room temperature i.e. at temperature boiler off. The tile changes to the unknown unknown

I don't think I'm going to be able to offer any more advice as I actually gave up on my HRT4-ZW a while back. The wakeup time of the device was too long for me to make it usable and it was always a bit of a PITA to setup with the ASR receiver box too.

I then questioned the need to have a physical thermostat at all. I now just use the ASR box to switch the boiler and control it directly from Hubitat with a virtual thermostat which takes it's temperature from existing multisensors in the system.

I have a wall mounted tablet with my heating dashboard page on to give easy access to all heating options.