HRT4-ZW or SRT321 Thermostat driver?

I did see the virtual thermostat app on various other posts and see your point on not really 'modern' to have just one point of control. I suppose I am just preempting the mrs complaining about it being cold and to turn up 'the thermostat', she loves the fully automated automatic lights on / off but still moans about it being too complicated despite her talking to it and it doing / doing automatically!
I suppose I was looking at individual room control for underfloor heating and thinking it would all be good /nosey to be on the system but I suppose this just working local isn't too much an issue.

I've been having a play with the driver for this. It seems that it's the variable thermostatmodeset that is causing the tile to change. In line 88 i've changed it from
map.value = "off"

to

map.value = "heat"

now the tile stays with the up and down arrows, reports both from tile to stat(on wake up), and then from stat to tile.

It seems that the tile is changing to this variable. I'm new to this system so don't know where the tile code is (if accessible)
Alternatively is it easy just to add a new variable and reference from this for the stat being on and off for rule purposes does anybody know?

Ureka, of a kind..... Setting a rule machine to create a rule where it compares the setpoint to the thermometer reading of the device in question.

Heat setpoint of stat1 is > stat1 TRUE
Heat setpoint of stat1 is <= stat1 FALSE

Bit of a faff, but it works from setpoint to control a boiler relay

It's all so long ago I really can't remember anything about that old code, if it's my version from above you are using. But it sounds like you've had more success with it than I did. :grinning:

I'm a bit confused though. If you are effectively taking control from the stat to switch the boiler directly using RM rules, what is the point of the stat now anyway? Isn't it now just a glorified temperature sensor? Also you might need to make the rules a bit more sophisticated in terms of hysteresis, or you will probably find the boiler switching on and off too often.

The built in virtual thermostat driver has an option for setting the hysteresis.

Something odd must be happening between the stat and driver. I don't know why the dashboard tile would be changing
"thermostatMode" which in the case of your thermostat should always be heat or off as it doesn't have a cooling option.

ThermostatOperatingState is what determines if the stat is calling for heat from the boiler or not and will be either heating or idle. Does that change correctly in your modified driver? If it does you can use that to control the boiler and that will have the stat' s built in hysteresis and the flame icon on the stat will match the boiler switch state too, which it won't if you control it independently.

Ah interesting point you make. The thermostat operating state variable remains unchanged, its the thermostat mode that is changing... Perhaps that's why the tile is changing because it thinks its going into a different mode rather than state. If I swap over the 2 variables in the driver that may just do it?
I see what your saying about the hysteresis however if the device only wakes up every few minutes to report back that's a big time delay swing over or under. I didn't realise the time for reporting on this device. As previously mentioned I come from a Vera backing where you can associate devices to groups whereby the devices talk amongst their group without controller as well as the controller having overall charge. This meant that the boiler relay was linked to the start via the controller forming an association meaning I didn't need to define on off rules, it just worked straight form the stay with no time issue.. Forward one back a couple, will keep chipping away :slight_smile:

You may be right that the driver is switching the wrong variable - that would certainly explain the dashboard tile behaviour.

I think you can get the boiler switch directly linked to the thermostat, so that it switches independently of Hubitat, I'm sure I had it connected that way at one point, but it was a total PITA to set up! I certainly remember reading through pages and pages of Horstmann instructions for doing it, together with online guides but a lot of them seemed contradictory.

It's the fact that I think the shortest wakeup time you can set is 6 minutes that was the final nail in the coffin for me. Because if Hubitat updates the setpoint and someone manually adjusts the stat before it has updated, what happens then? How do you deal with that scenario?

It is entirely your choice obviously, but I think you will save yourself a lot of frustration, by putting the HRT4_ZW in the "drawer of many gadgets" (where mine is now!) and just going down another thermostat route. You could even just use an old phone to display a heating dashboard with a thermostat tile for use as your physical stat and that would have instant 2 way updates for less than the cost of any thermostat.

Yeah I'm reading the principle of operation manual for the HRT4 trying to link the relay and stat together, then associating or associating control device to the Hubitat.
I know I'm banging my head against the wall, but my man drawer is full! it must must work!!
As far as the update either way, i'm finding that it's working pretty quick. If i update the state physically it updates the tile straight away. If i update the tile, on next wake-up the stat updates. so there isn't really a grey area situation. It will either update instantly, or delayed depending on which way comms is going. Do you have a link to any useful resource for me to understand how whatever the language used as to how arguments are formed and variables are read from the zwave network and allocated so I know which of the operating mode vs operating state i need to change around ? The language looks like a java based? I'm from a BASIC background so it flows and makes sense but some bits look weird!

Right just worked out which way you have to do it.... Ensure the relay is unassociated.

Associate the thermostat using L to be found, followed by n to be initialised and recognised.
dip switch down, then back up, set the driver to the str one and change wake up to 120, n function again, then dip down.
dip up scroll to i then include to boiler relay pressing the >||< dip down.

boiler relay is now running from the thermostat, but the stat is taking to HE and reporting / receiving values...... now just to sort out the mode / operating issue and jobs a good'un.... something less for the man drawer! more wall decoration lol

1 Like

for simplicity i could just leave line 88 as "heat" as the relay is working with the stat and stat can be controlled, but I'm going to try make it right for anybody else looking and falling in the same situation as all of us in this thread!

Well done so far!

I'm not much help on the detailed coding I'm afraid. I can just about understand roughly what existing code does, but I'm basically a cut and paste coder not an expert by any means.

In terms of the logic of the driver though, does it change thermostat mode from "heat" to "off" when the stat switches the boiler on and off? If so, it seems like if that part of the code that updates thermostatMode was edited to change ThermostatOperatingState between "heating" and "idle" instead, that might sort it.

yeah that is the lines I am going down, too. I just wasn't sure which particular argument is the right one as I see there is several, but that seems to be an update for a polling, updating device > HE, HE > Device and others... I'm sure i'll crack it in a bit... Just trying to add another stat and boiler first to ensure it wasn't just first unit luck!!

Like all programming languages I've tinkered with, all you've gotta do is change something, measure the outcome.. if it works then learn why, if not revert and try again.
This is really easy - Change line 93 to the new state from
map.name = "thermostatMode"

to

map.name = "thermostatOperatingState"

The tile stays the same as it's not the mode variable changing, but you can use the operating state to indicate the stat is on!

1 Like

To make the tile indicate correctly and follow the rules defined for the state of idle, heating, cooling etc you need to rename line 88 from "off" to "idle" and likewise with line 91 from "heat" to "heating" it seems there are rules to be followed to fit defined terms. You can't simply have a variable with anything written in there, like "phew its cool" or "jees it hawwt" but because it falls in line with standard definitions the thermostat tile changes to red when it's heating, and then back to grey when off.

Therefore update to @simon's code from line 83 to 95 is

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

2 Likes

Good stuff. :+1:

Might be worth posting your full working driver on here for future reference.

I might even dig my stat out from the box and give it a test!

Updated code working fully with tiles on dashboard and operating correctly with a boiler relay directly associated with thermostat:

First need to ensure both boiler relay and thermostat are unassociated with everything. Drop the below code into a user driver.

Put thermostat into programming mode DIP 1 up.
Start a Z wave discovery in HE

Associate the thermostat using L to be found, then followed by n to be initialised and recognised.
dip switch down, wait a bit then back up,
set the driver to custom user STR31, re wake the thermostat by the n function again, then dip down.
dip up scroll to i then include to boiler relay pressing the >||< dip down.

When adjusting the tile it will take 10 minutes (or whatever the wake up time is set to)to update the thermostat. If you adjust the thermostat physically it'll update the tile straight away. Because the Relay is linked direct to the thermostat there is no delay in the switching and the thermostat hysteresis is retained.

All the thermostat functions and association meanings can be found here http://horstmann.securemeters.com/files/3514/7012/0142/HRT4-ZW_Manual_Z-Wave_Information.pdf

/*
SRT321 Thermostat by MeavyDev - Converted for Hubitat by Scruffy-SJB
Further modification by ChrisBM 3-8-19 for incorrect thermostat mode operation, updated heating states to work with tiles 
*/
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 = "idle"
break
case hubitat.zwave.commands.thermostatmodev1.ThermostatModeSet.MODE_HEAT:
map.value = "heating"
}
map.name = "thermostatOperatingState"
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 = '600' // set default 10 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

Great work Chris. You've achieved more in a couple of days on this than I managed in weeks when I was trying. :grinning:

1 Like

....I get a little fixated with things when I start :sunglasses:

1 Like

Just to clarify, if you change the setpoint in Hubitat it still takes up to 10 minutes for the stat to update and therefore the boiler to react to the change?

Of a fashion yes. If you have a schedule set up it'll take 10 mins to update the new temperatures from either being set in HE via scheduler or Tile dashboard. (its when the thermostat is set to wake up to ping the HE and say hi, im here, HE then transmits new temp) I'm not sure how much sooner you could update this but that would be at cost of battery level because the zwave kit is needing to wake up. If you change the physical thermostat though the change is instantaneous to both the boiler and HE