Remotec Z-Thermostat (configuration with Z-Wave Commands)

I purchased a Remotec Z-Thermostat ZTS-110 and have it working with the standard Thermostat Application. However there are some parameters In the Thermostat not accessible from the front panel I would like to change. The manual (here) indicates that these parameters can be set with Z-Wave commands. Thing is, I have no idea how to proceed to create an app or device so I can change some of these parameters. Questions:

  1. Is it possible to create a device (or app) with a configuration option to set some of these parameters: Specifically I would like to change Trigger AUTO report if temperature change is <= 1 degree F (not the default of 4 degrees F) and set the Sensor temperature calibration (it reads 2 degrees high)
  2. Assuming it is "possible:, can someone point me to a device or app that has similar functionality (Sending commands to a device using Z-Wave).
  3. Am I wasting my time to try to learn enough about device drivers to create one for this Thermostat that has the optional configuration commands. I am a competent C++ programmer so the only issue is finding the documentation and or examples to proceed.

I am new to Smart Things and just go my starter kit so I am just learning how to create apps. I have not started developing a new “device” for this Thermostat but am willing to learn and try and have the time.

1 Like

Yes, a device will work

Every zwave devicetype does this, the Z-Wave Thermostat devicetype would be a good place to start.
https://graph.api.smartthings.com/ide/device/create > From template > Z-Wave Thermostat

Thanks for reply.
I am learning and getting a feel for devices. The manual for my thermostat states:
“you may use the below configuration parameters to change settings of corresponding functionality” Then there is a table that lists all the things that can be changed.
Here are two I would like to “set” differently than the Z-Thermostat’s defaults:
Function ------------------------- parameter number ----value I want to set
Sensor temperature calibration – 13 (0x0D) -------- 1 (0x01) <— Add one degree to display)
Auto Report by time interval ------ 12 (0x0C) -------- 1 (0x01) <— 0.5 hrs default is 2 = 1.0 hr

I will want to modify the Configure section so when I click configure the above commands are sent.

To help get me pointed in the right direction:
Would I use the Z-Wave command class “Configuration 0x70” with parameters for the above desired results or one of the other Z-Wave command classes?

Are there any examples with this “configure” capability?

There are two options:

A. You can do it manually with the simulator. Change the configure() method to something like

def configure() {
	delayBetween([
		zwave.configurationV1.configurationSet(parameterNumber: 13, size: 1, scaledConfigurationValue: 1).format(),
		zwave.configurationV1.configurationSet(parameterNumber: 12, size: 1, scaledConfigurationValue: 1).format(),
	], 1500)
}

Then run it in the simulator and click the configure button on the right. Once that’s been sent you can go back to using the official device handler.

B. If you want to do it “right” with editable configuration in the device preferences, you should look at the Aeon Siren example, specifically the metadata.preferences section and the updated() method.

1 Like

Thanks Duncan. I will give those ideas a try in the next week or so.

Dennis

Got it working! Thanks for the help

Hi Dennis,

Ive got a couple of these ZTS-110 as well and am interested in how you were able to send the config commands. Did you use the simulator or write a editable configuration? If the latter can you post the code you used? It would help me out greatly.

Thanks,

Tim

I just changed the configure part of the Z-Wave Thermostat template and installed that (test with device, not virtual) I don’t eve understand the virtual stuff yet. The code in configure sections gets executed when you install or update as I understand. Here is the modified code. (not sure if you need to comment out the first section but I was experimentings

def configure() {
// delayBetween([
// zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
// zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(),
// zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()
// ], 2300)
// parm 13 = calibration -2,-1,0,1,2 Etc
// parm 12 = aout report 0 = never, 1 = 30 min, 2 = 1 hr (default)
// parm 11 Traiger autoreport if room temp different from last report (report temp only
// 1 = 1 deg F, 2 = 2 degree F, 3 = 3, 4 = 4 deg F = default Set to 1
delayBetween([
zwave.configurationV1.configurationSet(parameterNumber: 13, size: 1, scaledConfigurationValue: 0 ).format(),
zwave.configurationV1.configurationSet(parameterNumber: 12, size: 1, scaledConfigurationValue: 1 ).format(),
zwave.configurationV1.configurationSet(parameterNumber: 11, size: 1, scaledConfigurationValue: 1 ).format(),
], 1500)

// delayBetween([
// zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
// zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
// ], 2300)
}

OK, I “almost” have my device handler done for the Remotec ZTS-110 Thermostat. But I am perplexed by the behavior. When I updated the device in my android app and enter the settings - the settings are correctly set (by observing the debug statements) but, even though I call the configure() routine in the updated() routine, the settings do not take in the actual thermostat. I know because the refresh() routine asks for the current setting (in my case, parameter 13 - the calibration) and updates the tile with the returned value. What does work is to click the Configure icon in the app - which as far as I can tell, just calls the configure() routine which does the same commands as when called from the updated() routine. So what am I missing.
Notes:
I added an attribute “Calibration”, "number"
and added a standard tile to display the attribute
standardTile(“Calibration”, “device.Calibration”, decoration: “flat”) {
state “Calibration”, label:‘Cal: ${currentValue}’, action:"configuration.configure"
and added other things throughout all with //DES … on the comments

The code:

Here are ONLY the routines related to this question (-Wave Thermostat handler)

metadata {
	// Automatically generated. Make future change here.
	definition (name: "ZTS-110 Thermostat", namespace: "dspanogle", author: "SmartThings + DES") {
		capability "Actuator"
		capability "Temperature Measurement"
		capability "Relative Humidity Measurement"
		capability "Thermostat"
		capability "Configuration"
		capability "Polling"
        attribute "Calibration", "number"  // DES added
		command "switchMode"
		command "switchFanMode"
        command "quickSetCool"
        command "quickSetHeat"
        // DES replaced fingerprint to match the raw descrption from theRomotec ZTS-110
		// fingerprint deviceId: "0x08"
		// fingerprint inClusters: "0x43,0x40,0x44,0x31"
        // raw description for ZTS-110: 	0 0 0x1001 0 0 0 4 0x25 0x27 0x86 0x72
        fingerprint deviceId: "0x1001"
		fingerprint inClusters: "0x25,0x27,0x86,0x72", manufacturer: "Remotec", model: "ZTS-110"
        
	}

	tiles {
		valueTile("temperature", "device.temperature", width: 2, height: 2) {
			state("temperature", label:'${currentValue}°',
				backgroundColors:[
					[value: 31, color: "#153591"],
					[value: 44, color: "#1e9cbb"],
					[value: 59, color: "#90d2a7"],
					[value: 74, color: "#44b621"],
					[value: 84, color: "#f1d801"],
					[value: 95, color: "#d04e00"],
					[value: 96, color: "#bc2323"]
				]
			)
		}
		standardTile("mode", "device.thermostatMode", decoration: "flat") {
			state "off", label:'${name}', action:"switchMode", nextState:"to_heat"
			state "heat", label:'${name}', action:"switchMode", nextState:"to_cool"
			state "cool", label:'${name}', action:"switchMode", nextState:"..."
			state "auto", label:'${name}', action:"switchMode", nextState:"..."
			state "emergencyHeat", label:'${name}', action:"switchMode", nextState:"..."
			state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool"
			state "to_cool", label: "cool", action:"switchMode", nextState:"..."
			state "...", label: "...", action:"off", nextState:"off"
		}
		standardTile("fanMode", "device.thermostatFanMode", decoration: "flat") {
			state "fanAuto", label:'${name}', action:"switchFanMode"
			state "fanOn", label:'${name}', action:"switchFanMode"
			state "fanCirculate", label:'${name}', action:"switchFanMode"
		}
		controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2) {
			state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00"
		}
		valueTile("heatingSetpoint", "device.heatingSetpoint", decoration: "flat") {
			state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff"
		}
		controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2) {
			state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb"
		}
		valueTile("coolingSetpoint", "device.coolingSetpoint", decoration: "flat") {
			state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff"
		}
		standardTile("refresh", "device.thermostatMode", decoration: "flat") {
			state "default", action:"polling.poll", icon:"st.secondary.refresh"
		}
		standardTile("configure", "device.configure", decoration: "flat") {
			state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
		}
        // DES added This tile to match attribute attribute "Calibration", "number"  
		standardTile("Calibration", "device.Calibration", decoration: "flat") {
			state "Calibration", label:'Cal: ${currentValue}', action:"configuration.configure"
        }
		main "temperature"
		details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "configure", "Calibration"])
      
	}
}
//  DES add preferences
//  ** Get inputs for the configuration (only 3 of the possible configurations are included at this time)
preferences {
       //   parm 13 = calibration  -3,-2,-1,0,1,2, 3 Etc
       input "cal", "number", title: "Calibration",
              description: "Offset -2,-1,0,1,2 Etc", defaultValue: 0,
              required: false, displayDuringSetup: true  // also displayed during edit in mobile app
       //   parm 12 = autoreport time trigger:  0 = never, 1 = 30 min, 2 = 1 hr (default) 
       input "RptTime", "number", title: "Report Time",
              description: "0=Never, 1=30min, 2=1hr (default)", defaultValue: 2,
              required: false, displayDuringSetup: true
       //   parm 11 Trigger autoreport if room temp different from last report 
       //          1 = 1 deg F,  2 = 2 degree F,  3 = 3,  4 = 4 deg F = default   Set to 1
       input "RptDelta", "number", title: "Report when temp diff is (degF)",
              description: " 1=1deg, 2=2deg, 3=3deg, 4=4deg (default)", defaultValue: 4,
              required: false, displayDuringSetup: true
}
// DES add Configuration Event Generation
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
    log.debug " in zwave Event ConfigurationReport"
	def map = [:]
    switch (cmd.parameterNumber) {
        // If the device is reporting Calibration
        case 13:
            map.name = "Calibration"
            def offset = cmd.configurationValue[0]
            if (offset > 128)
            {
              offset = offset - 256            
            }
            map.value = offset
            log.debug " did Cal map cal = ${map.value}"
        	break
	}
      map
}
// DES added updated()
def updated() {
	log.trace "Updated: Settings: ${settings}"
	log.trace"Updated: State:  ${state.Configuration}"
	configure()  // Call the configure routine to send the configuration commands
}

// DES modified configure() 
def configure() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(),
		zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()
	], 2300)
//   parm 13 = calibration  -2,-1,0,1,2 Etc
//   parm 12 = Time elapse auto report 0 = never, 1 = 30 min, 2 = 1 hr (default) 
//   parm 11 Trigger autoreport if room temp different from last report  
//          1 = 1 deg F,  2 = 2 degree F,  3 = 3,  4 = 4 deg F (default) 
    log.debug "Setting Configuration cal=${settings.cal}, RptTime= ${settings.RptTime}, RptDelta = ${settings.RptDelta}"   //des
	delayBetween([
		zwave.configurationV1.configurationSet(parameterNumber: 13, size: 1, scaledConfigurationValue: settings.cal ).format(),
		zwave.configurationV1.configurationSet(parameterNumber: 12, size: 1, scaledConfigurationValue: settings.RptTime ).format(),
	        zwave.configurationV1.configurationSet(parameterNumber: 11, size: 1, scaledConfigurationValue: settings.RptDelta ).format(),
  	], 2300)
}
// DES modified Command Implementation polling
def poll() {
	// DES  add poll of calibration parameter 13 
    delayBetween([
		zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
		zwave.thermostatModeV2.thermostatModeGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format(),
        zwave.configurationV1.configurationGet(parameterNumber: 13).format() 
	], 2300)
}

def parse(String description)
{
   // DES added 0x70: COMMAND_CLASS_CONFIGURATION
	def map = createEvent(zwaveEvent(zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3,  0x70: 1]))) // DES added 0x70, 1
	if (!map) {
		return null
	}

	def result = [map]
	if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) {
		def map2 = [
			name: "thermostatSetpoint",
			unit: getTemperatureScale()
		]
		if (map.name == "thermostatMode") {
			state.lastTriedMode = map.value
			if (map.value == "cool") {
				map2.value = device.latestValue("coolingSetpoint")
				log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}"
			}
			else {
				map2.value = device.latestValue("heatingSetpoint")
				log.info "THERMOSTAT, latest heating setpoint = ${map2.value}"
			}
		}
		else {
			def mode = device.latestValue("thermostatMode")
			log.info "THERMOSTAT, latest mode = ${mode}"
			if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) {
				map2.value = map.value
				map2.unit = map.unit
			}
		}
		if (map2.value != null) {
			log.debug "THERMOSTAT, adding setpoint event: $map"
			result << createEvent(map2)
		}
	} else if (map.name == "thermostatFanMode" && map.isStateChange) {
		state.lastTriedFanMode = map.value
	}
	log.debug "Parse returned $result"
	result
}

Thanks for posting your code Dennis. I wish I could be more help but I am very new to Smartthings and the groovy language. I’m just trying to get this same thermostat working (or at least configured) like you. Here is the Device Handler that I have been using that works very well It is just missing the preferences setting code that you are trying to get working. I hope it helps.

/* Better-Thermostat.device.groovy
 *
 * Variation of the stock SmartThings "Zwave-Thermostat"
 *
 * Device type removes the sliders and replaces them with incremental
 * up and down buttons on each side of the heating and cooling setpoints.
 *
 * To use you must have IDE access on your acount. Add a new device
 * type and add the custom commands:
 * 		heatLevelUp
 *		heatLevelDown
 * 		coolLevelUp
 *		coolLevelDown
 *		switchMode
 *		switchFanMode
 * Replace the starter code with this code and save the file. Go into
 * "My devices" and select the thermostat you want to change. Select "Edit"
 * and then change the "Type" to use this device type.
 *
 * Happy Hacking!
 *
 * twack@wackware.net
 * 20140209
 *
*/
metadata {
	// Automatically generated. Make future change here.
	definition (name: "ZTS-110", author: "todd@wackford.net") {
		capability "Temperature Measurement"
		capability "Refresh"
		capability "Thermostat"
		capability "Configuration"
		capability "Polling"

		command "heatLevelUp"
		command "heatLevelDown"
		command "coolLevelUp"
		command "coolLevelDown"
		command "switchMode"
		command "switchFanMode"
	}

	// simulator metadata
	simulator {
		status "off"			: "command: 4003, payload: 00"
		status "heat"			: "command: 4003, payload: 01"
		status "cool"			: "command: 4003, payload: 02"
		status "auto"			: "command: 4003, payload: 03"

		status "fanAuto"		: "command: 4403, payload: 00"
		status "fanOn"			: "command: 4403, payload: 01"
		status "fanCirculate"	: "command: 4403, payload: 06"

		status "heat 60"        : "command: 4303, payload: 01 01 3C"
		status "heat 68"        : "command: 4303, payload: 01 01 44"
		status "heat 72"        : "command: 4303, payload: 01 01 48"

		status "cool 72"        : "command: 4303, payload: 02 01 48"
		status "cool 76"        : "command: 4303, payload: 02 01 4C"
		status "cool 80"        : "command: 4303, payload: 02 01 50"

		status "temp 58"        : "command: 3105, payload: 01 22 02 44"
		status "temp 62"        : "command: 3105, payload: 01 22 02 6C"
		status "temp 70"        : "command: 3105, payload: 01 22 02 BC"
		status "temp 74"        : "command: 3105, payload: 01 22 02 E4"
		status "temp 78"        : "command: 3105, payload: 01 22 03 0C"
		status "temp 82"        : "command: 3105, payload: 01 22 03 34"

		status "idle"			: "command: 4203, payload: 00"
		status "heating"		: "command: 4203, payload: 01"
		status "cooling"		: "command: 4203, payload: 02"
		status "fan only"		: "command: 4203, payload: 03"
		status "pending heat"	: "command: 4203, payload: 04"
		status "pending cool"	: "command: 4203, payload: 05"
		status "vent economizer": "command: 4203, payload: 06"

		// reply messages
		reply "2502": "command: 2503, payload: FF"
	}

	tiles {
		valueTile("temperature", "device.temperature", width: 2, height: 2) {
			state("temperature", label:'${currentValue}°', unit:'F',
				backgroundColors:[
					[value: 31, color: "#153591"],
					[value: 44, color: "#1e9cbb"],
					[value: 59, color: "#90d2a7"],
					[value: 74, color: "#44b621"],
					[value: 84, color: "#f1d801"],
					[value: 95, color: "#d04e00"],
					[value: 96, color: "#bc2323"]
				]
			)
		}
		standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
			state "off", label:'', action:"switchMode", icon:"st.thermostat.heating-cooling-off"
			state "heat", label:'', action:"switchMode", icon:"st.thermostat.heat"
			state "cool", label:'', action:"switchMode", icon:"st.thermostat.cool"
			state "auto", label:'', action:"switchMode", icon:"st.thermostat.auto"
		}
		standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
			state "fanAuto", label:'', action:"switchFanMode", icon:"st.thermostat.fan-auto"
			state "fanOn", label:'', action:"switchFanMode", icon:"st.thermostat.fan-on"
			state "fanCirculate", label:'  ', action:"switchFanMode", icon:"st.thermostat.fan-circulate"
		}
		valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
			state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
		}
		valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
			state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
		}
		standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
			state "default", action:"polling.poll", icon:"st.secondary.refresh"
		}
        standardTile("heatLevelUp", "device.heatingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
                        state "heatLevelUp", label:'  ', action:"heatLevelUp", icon:"st.thermostat.thermostat-up"
        }
        standardTile("heatLevelDown", "device.heatingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
                        state "heatLevelDown", label:'  ', action:"heatLevelDown", icon:"st.thermostat.thermostat-down"
        }
        standardTile("coolLevelUp", "device.heatingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
                        state "coolLevelUp", label:'  ', action:"coolLevelUp", icon:"st.thermostat.thermostat-up"
        }
        standardTile("coolLevelDown", "device.heatingSetpoint", canChangeIcon: false, inactiveLabel: false, decoration: "flat") {
                        state "coolLevelDown", label:'  ', action:"coolLevelDown", icon:"st.thermostat.thermostat-down"
        }
        main "temperature"
		details(["temperature", "mode", "fanMode", "heatLevelDown", "heatingSetpoint", "heatLevelUp", "coolLevelDown", "coolingSetpoint", "coolLevelUp", "refresh", "configure"])
	}

}


def coolLevelUp(){
    int nextLevel = device.currentValue("coolingSetpoint") + 1
    
    if( nextLevel > 99){
    	nextLevel = 99
    }
    log.debug "Setting cool set point up to: ${nextLevel}"
    setCoolingSetpoint(nextLevel)
}

def coolLevelDown(){
    int nextLevel = device.currentValue("coolingSetpoint") - 1
    
    if( nextLevel < 50){
    	nextLevel = 50
    }
    log.debug "Setting cool set point down to: ${nextLevel}"
    setCoolingSetpoint(nextLevel)
}

def heatLevelUp(){
    int nextLevel = device.currentValue("heatingSetpoint") + 1
    
    if( nextLevel > 90){
    	nextLevel = 90
    }
    log.debug "Setting heat set point up to: ${nextLevel}"
    setHeatingSetpoint(nextLevel)
}

def heatLevelDown(){
	int nextLevel = device.currentValue("heatingSetpoint") - 1
    
    if( nextLevel < 40){
    	nextLevel = 40
    }
    log.debug "Setting heat set point down to: ${nextLevel}"
    setHeatingSetpoint(nextLevel)
}

def parse(String description)
{
	def map = createEvent(zwaveEvent(zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3])))
	if (!map) {
		return null
	}

	def result = [map]
	if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) {
		def map2 = [
			name: "thermostatSetpoint",
			unit: "F"
		]
		if (map.name == "thermostatMode") {
			updateState("lastTriedMode", map.value)
			if (map.value == "cool") {
				map2.value = device.latestValue("coolingSetpoint")
				log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}"
			}
			else {
				map2.value = device.latestValue("heatingSetpoint")
				log.info "THERMOSTAT, latest heating setpoint = ${map2.value}"
			}
		}
		else {
			def mode = device.latestValue("thermostatMode")
			log.info "THERMOSTAT, latest mode = ${mode}"
			if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) {
				map2.value = map.value
				map2.unit = map.unit
			}
		}
		if (map2.value != null) {
			log.debug "THERMOSTAT, adding setpoint event: $map"
			result << createEvent(map2)
		}
	} else if (map.name == "thermostatFanMode" && map.isStateChange) {
		updateState("lastTriedFanMode", map.value)
	}
	log.debug "Parse returned $result"
	result
}

// Event Generation
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.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;
		case 2:
			map.name = "coolingSetpoint"
			break;
		default:
			return [:]
	}
	// So we can respond with same format
	state.size = cmd.size
	state.scale = cmd.scale
	state.precision = cmd.precision
	map
}

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

def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd)
{
	def map = [:]
	switch (cmd.operatingState) {
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE:
			map.value = "idle"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING:
			map.value = "heating"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_COOLING:
			map.value = "cooling"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY:
			map.value = "fan only"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT:
			map.value = "pending heat"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL:
			map.value = "pending cool"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER:
			map.value = "vent economizer"
			break
	}
	map.name = "thermostatOperatingState"
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) {
	def map = [:]
	switch (cmd.mode) {
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF:
			map.value = "off"
			break
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
			map.value = "heat"
			break
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL:
			map.value = "cool"
			break
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO:
			map.value = "auto"
			break
	}
	map.name = "thermostatMode"
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) {
	def map = [:]
	switch (cmd.fanMode) {
		case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW:
			map.value = "fanAuto"
			break
		case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW:
			map.value = "fanOn"
			break
		case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION:
			map.value = "fanCirculate"
			break
	}
	map.name = "thermostatFanMode"
	map.displayed = false
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
	def supportedModes = ""
	if(cmd.off) { supportedModes += "off " }
	if(cmd.heat) { supportedModes += "heat " }
	if(cmd.cool) { supportedModes += "cool " }
	if(cmd.auto) { supportedModes += "auto " }

	updateState("supportedModes", supportedModes)
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) {
	def supportedFanModes = ""
	if(cmd.auto) { supportedFanModes += "fanAuto " }
	if(cmd.low) { supportedFanModes += "fanOn " }
	if(cmd.circulation) { supportedFanModes += "fanCirculate " }

	updateState("supportedFanModes", supportedFanModes)
}

def updateState(String name, String value) {
	state[name] = value
	device.updateDataValue(name, value)
}

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

def zwaveEvent(physicalgraph.zwave.Command cmd) {
	log.warn "Unexpected zwave command $cmd"
}

// Command Implementations
def poll() {
	delayBetween([
		zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
		zwave.thermostatModeV2.thermostatModeGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
	], 2300)
}

def setHeatingSetpoint(degreesF) {
	setHeatingSetpoint(degreesF.toDouble())
}

def setHeatingSetpoint(Double degreesF) {
	def p = (state.precision == null) ? 1 : state.precision
	delayBetween([
		zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: 1, precision: p, scaledValue: degreesF).format(),
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()
	])
}

def setCoolingSetpoint(degreesF) {
	setCoolingSetpoint(degreesF.toDouble())
}

def setCoolingSetpoint(Double degreesF) {
	def p = (state.precision == null) ? 1 : state.precision
	delayBetween([
		zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: 1, precision: p,  scaledValue: degreesF).format(),
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()
	])
}

def configure() {


	delayBetween([
    	//Tim Code Start
        zwave.configurationV1.configurationSet(parameterNumber: 12, size: 1, scaledConfigurationValue: 1).format(),
        zwave.configurationV1.configurationSet(parameterNumber: 11, size: 1, scaledConfigurationValue: 1).format(),
        zwave.configurationV1.configurationSet(parameterNumber: 13, size: 1, scaledConfigurationValue: 0).format(),
        //Tim Code End
    
		zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(),
		zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()
	], 2300)
}

def modes() {
	["off", "auto", "heat", "cool"]
}

def switchMode() {
	def currentMode = device.currentState("thermostatMode")?.value
	def lastTriedMode = getDataByName("lastTriedMode") ?: currentMode ?: "off"
	def supportedModes = getDataByName("supportedModes")
	def modeOrder = modes()
	def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
	def nextMode = next(lastTriedMode)
	if (supportedModes?.contains(currentMode)) {
		while (!supportedModes.contains(nextMode) && nextMode != "off") {
			nextMode = next(nextMode)
		}
	}
    log.debug "Switching to mode: ${nextMode}"
	switchToMode(nextMode)
}

def switchToMode(nextMode) {
	def supportedModes = getDataByName("supportedModes")
	if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
	if (nextMode in modes()) {
		updateState("lastTriedMode", nextMode)
		return "$nextMode"()
	} else {
		log.debug("no mode method '$nextMode'")
	}
}

def switchFanMode() {
	def currentMode = device.currentState("thermostatFanMode")?.value
	def lastTriedMode = getDataByName("lastTriedFanMode") ?: currentMode ?: "off"
	def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn"
	def modeOrder = ["fanAuto", "fanCirculate", "fanOn"]
	def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
	def nextMode = next(lastTriedMode)
	while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") {
		nextMode = next(nextMode)
	}
	switchToFanMode(nextMode)
}

def switchToFanMode(nextMode) {
	def supportedFanModes = getDataByName("supportedFanModes")
	if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"

	def returnCommand
	if (nextMode == "fanAuto") {
		returnCommand = fanAuto()
	} else if (nextMode == "fanOn") {
		returnCommand = fanOn()
	} else if (nextMode == "fanCirculate") {
		returnCommand = fanCirculate()
	} else {
		log.debug("no fan mode '$nextMode'")
	}
	if(returnCommand) updateState("lastTriedFanMode", nextMode)
	returnCommand
}

def getDataByName(String name) {
	state[name] ?: device.getDataValue(name)
}

def getModeMap() { [
	"off": 0,
	"heat": 1,
	"cool": 2,
	"emergency heat": 4
]}

def setThermostatMode(String value) {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	])
}

def getFanModeMap() { [
	"auto": 0,
	"on": 1,
	"circulate": 6
]}

def setThermostatFanMode(String value) {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	])
}

def off() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	])
}

def heat() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	])
}

def cool() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	])
}

def auto() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	])
}

def fanOn() {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	])
}

def fanAuto() {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	])
}

def fanCirculate() {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	])
}

PS I didn’t know how to post code in the scrolling box either until just now after reading this post : How to post code?

Tim

Duncan…
Could you take another look at my configuration attempts on this. I am almost there and ready for clean up and UI tweaks but… I seem to be missing something trying to get configure() to work after updated(). See my last post prior to Tim’s

Thanks

Dennis

The short answer: change the last line of updated() to response(configure())

The long answer is: calling format() on a command doesn’t actually send it. You have to return all the commands you want to send from a method – when the system executes a command like configure it takes the return value and sends it as a command via the hub. The updated() method is not a command, so it doesn’t do that by default, you have to specifically return a HubAction object. The response() helper wraps commands up in a HubAction so they can be sent from parse() or updated().

Sorry we know it’s a confusing interface, but we haven’t had a chance to redo it yet.

That did the trick - thanks Duncan !

Only two peculiar phenomenon remain unexplained:

  1. When the device is edited (in the app) and the preferences inputs are entered, the routine updated() gets executed two times. It only gets executed one time on handler installation.
    2.The displayed values of the inputs when the device is edited (in the app) are those last entered, not the changed settings values after I change settings in updated() (to limit input values to those allowed).
    It would be nice to know why and how to fix those peculiarities. Anyone have a clue as to why, please respond and I will attempt to fix the code.
    CHANGES MADE:
    I added battery status (in case anyone actually uses a battery instead of 24 volts)
    I also added three configuration settings: Calibration; Auto report after elapsed time; Auto report on temperature change.
    I removed the 'Configure" tile as it is not needed. configure is done on installation or device edit (in the app).
    There are many other configuration parameters for this thermostat but I only added the ones I wanted to change. However, the code gives others enough of an example as to how to add additional configurations inputs, setting and displaying the values. I invite others to continue enhancing the code.

For those that want to use this device handler, I include the code below:

/**
 *  ZTS-110 Thermostat by Remotec
 *
 *  Copyright 2016 Dennis Spanogle / Template used Copyright Smart Things
 *
 *  Modified Smart Things generic Z-Wave Thermostat
 *    added 3 configuration parameters  (for now)
 *    added battery status report (in case user selects battery)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *
 */
metadata {
	// Automatically generated. Make future change here.
	definition (name: "ZTS-110 Thermostat", namespace: "dspanogle", author: "SmartThings + Dennis Spanogle (DES)") {
		capability "Actuator"
		capability "Temperature Measurement"
		capability "Relative Humidity Measurement"
		capability "Thermostat"
		capability "Configuration"
		capability "Polling"
		capability "Sensor"
        capability "Battery"   // DES
		
		attribute "thermostatFanState", "string" //DES added , "string"  (This does not seem to be used)
        attribute "Calibration", "number"   // DES added 
        attribute "Autorpt_DegF", "number"  // DES added
        attribute "Autorpt_Hr", "number"    // DES added
		command "switchMode"
		command "switchFanMode"
        command "quickSetCool"
        command "quickSetHeat"
        // DES replaced fingerprint to match the raw descrption from theRomotec ZTS-110
		// fingerprint deviceId: "0x08"
		// fingerprint inClusters: "0x43,0x40,0x44,0x31"
        // raw description for ZTS-110: 	0 0 0x1001 0 0 0 4 0x25 0x27 0x86 0x72
        fingerprint deviceId: "0x1001"
		fingerprint inClusters: "0x25,0x27,0x86,0x72", manufacturer: "Remotec", model: "ZTS-110"
        
	}

	// simulator metadata
	simulator {
		status "off"			: "command: 4003, payload: 00"
		status "heat"			: "command: 4003, payload: 01"
		status "cool"			: "command: 4003, payload: 02"
		status "auto"			: "command: 4003, payload: 03"
		status "emergencyHeat"	: "command: 4003, payload: 04"

		status "fanAuto"		: "command: 4403, payload: 00"

		status "fanOn"			: "command: 4403, payload: 01"
		status "fanCirculate"	: "command: 4403, payload: 06"

		status "heat 60"        : "command: 4303, payload: 01 09 3C"
		status "heat 68"        : "command: 4303, payload: 01 09 44"
		status "heat 72"        : "command: 4303, payload: 01 09 48"

		status "cool 72"        : "command: 4303, payload: 02 09 48"
		status "cool 76"        : "command: 4303, payload: 02 09 4C"
		status "cool 80"        : "command: 4303, payload: 02 09 50"

		status "temp 58"        : "command: 3105, payload: 01 2A 02 44"
		status "temp 62"        : "command: 3105, payload: 01 2A 02 6C"
		status "temp 70"        : "command: 3105, payload: 01 2A 02 BC"
		status "temp 74"        : "command: 3105, payload: 01 2A 02 E4"
		status "temp 78"        : "command: 3105, payload: 01 2A 03 0C"
		status "temp 82"        : "command: 3105, payload: 01 2A 03 34"

		status "idle"			: "command: 4203, payload: 00"
		status "heating"		: "command: 4203, payload: 01"
		status "cooling"		: "command: 4203, payload: 02"
		status "fan only"		: "command: 4203, payload: 03"
		status "pending heat"	: "command: 4203, payload: 04"
		status "pending cool"	: "command: 4203, payload: 05"
		status "vent economizer": "command: 4203, payload: 06"

		// reply messages
		reply "2502": "command: 2503, payload: FF"
	}
    // DES changed the layout scale and height, width for all tiles
	tiles (scale: 2) {
		valueTile("temperature", "device.temperature", width: 4, height: 4) {
			state("temperature", label:'${currentValue}°',
				backgroundColors:[
					[value: 31, color: "#153591"],
					[value: 44, color: "#1e9cbb"],
					[value: 59, color: "#90d2a7"],
					[value: 74, color: "#44b621"],
					[value: 84, color: "#f1d801"],
					[value: 95, color: "#d04e00"],
					[value: 96, color: "#bc2323"]
				]
			)
		}
        //  DES removed depreciated inactiveLabel: false from all tiles
		standardTile("mode", "device.thermostatMode", decoration: "flat", width: 2, height: 2) {
			state "off", label:'${name}', action:"switchMode", nextState:"to_heat"
			state "heat", label:'${name}', action:"switchMode", nextState:"to_cool"
			state "cool", label:'${name}', action:"switchMode", nextState:"..."
			state "auto", label:'${name}', action:"switchMode", nextState:"..."
			state "emergencyHeat", label:'${name}', action:"switchMode", nextState:"..."
			state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool"
			state "to_cool", label: "cool", action:"switchMode", nextState:"..."
			state "...", label: "...", action:"off", nextState:"off"
		}
		standardTile("fanMode", "device.thermostatFanMode", decoration: "flat", width: 2, height: 2) {
			state "fanAuto", label:'${name}', action:"switchFanMode"
			state "fanOn", label:'${name}', action:"switchFanMode"
			state "fanCirculate", label:'${name}', action:"switchFanMode"
		}
		controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 4) {
			state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00"
		}
		valueTile("heatingSetpoint", "device.heatingSetpoint", decoration: "flat", width: 2, height: 1) {
			state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff"
		}
		controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 4) {
			state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb"
		}
		valueTile("coolingSetpoint", "device.coolingSetpoint", decoration: "flat", width: 2, height: 1) {
			state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff"
		}
		standardTile("refresh", "device.thermostatMode", decoration: "flat", width: 2, height: 2) {
			state "default", action:"polling.poll", icon:"st.secondary.refresh"
		}
        // DES since edit device in app returns configure, this tile is really not needed
		//standardTile("configure", "device.configure", decoration: "flat", width: 2, height: 2) {
			//state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
		//}
        // DES added This tile to match attribute attribute "Calibration", "number"  
        valueTile("Calibration", "device.Calibration", decoration: "flat", width: 2, height: 1) {
			state "Calibration", label:'Cal: ${currentValue}°'        //, action:"configuration.configure"
        }
        valueTile("Autorpt_Hr", "device.Autorpt_Hr", decoration: "flat", width: 2, height: 1) {
			state "Autorpt_Hr", label:'Rpt: ${currentValue}hr'        // , action:"configuration.configure"
        }        
		valueTile("Autorpt_DegF", "device.Autorpt_DegF", decoration: "flat", width: 2, height: 1) {
			state "Autorpt_DegF", label:'Rpt: ${currentValue}°F'      //, action:"configuration.configure"
        } 
		valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 1) {
			state "battery", label:'Bat: ${currentValue}%', unit:""
        }           

		main "temperature"
		details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", 
        "coolingSetpoint", "refresh", "configure", "Calibration", "Autorpt_DegF", "Autorpt_Hr", "battery" ])
      
	}
}
//  DES add preferences
//  ** Get inputs for the configuration (only 3 of the possible configurations are included at this time)
preferences {
       //   parm 13 = calibration  -3,-2,-1,0,1,2, 3 Etc
       input "cal", "number", title: "Calibration: -10°F to +10°F (0=default)",
              description: "Offset -2,-1,0 (default),1,2 Etc", defaultValue: 0,
              required: false  //, displayDuringSetup: true  // also displayed during edit in mobile app
       //   parm 12 = autoreport time trigger:  0 = never, 1 = 30 min, 2 = 1 hr (default) 
       input "RptTime", "number", title: "Auto Report Time: 0=Never, 1 to 16 half hrs  (2=default=1hr)",
              description: "0=Never, 1=.5hr, 2=1hr (default), 3=1.5hr, .. max 16=8hr", defaultValue: 2,
              required: false //, displayDuringSetup: true
       //   parm 11 Trigger autoreport if room temp different from last report 
       //          1 = 1 deg F,  2 = 2 degree F,  3 = 3,  4 = 4 deg F = default   Set to 1
       input "RptDelta", "number", title: "Auto Report °F Change: 0=Never, 1°F to 8°F (4=default)",
              description: "0=Never, 1=1°F, 2=2°F, 3=3°F, 4=4°F (default), .. max8=8°F", defaultValue: 4,
              required: false  //, displayDuringSetup: true
}
// DES add battery Event Generation
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
	def map = [ name: "battery", unit: "%" ]
	if (cmd.batteryLevel == 0xFF) {
		map.value = 1
		map.descriptionText = "${device.displayName} Low Battery"
		map.isStateChange = true
	} else {
		map.value = cmd.batteryLevel
	}
	state.lastbatt = now()
   // log.debug "Got Battery status: ${map}"
	map
}

// DES add Configuration Event Generation
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
	//log.debug " in zwave Event ConfigurationReport"
	def map = [:]
	def value = cmd.configurationValue[0]
	switch (cmd.parameterNumber) {
		case 11:
			map.name = "Autorpt_DegF"
			if (value < 0){value = 0}
            if (value > 8){value = 8}
			map.value = value
			break
		case 12:
			map.name = "Autorpt_Hr"
            if (value < 0){value = 0}   // DES These probably redundant
            if (value > 16){value = 16}
            map.value = value/2    // report in hours instead of half hours as entered.
            break
		case 13:
			map.name = "Calibration"
			if (value > 128){
				value = value - 256            
            }
			map.value = value
			//log.debug " did Cal map cal = ${map.value}"
			break
	}
      map
}
// DES added updated()
def updated() {
	log.trace "Updated Input Settings: ${settings}"
    if(settings.cal > 10){settings.cal = 10}  // calibration must be between -10 ann +10
    if(settings.cal < -10){settings.cal = -10}
    if(settings.RptTime > 16){settings.RptTime = 16} // Every 8 hours is max
    if(settings.RptTime <  0){settings.RptTime = 0}   // Note 0 disables auto report by time
    if(settings.RptDelta > 8){settings.RptDelta = 8}  // delta of 8 degrees is max
    if(settings.RptDelta < 0){settings.RptDelta = 0}  // Note 0 disables auto report if different than last  
    log.trace "Updated Corrected Settings: ${settings}"
    response(configure())
}

// DES modified configure() 
def configure() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(),
		zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()
	], 2300)
//   parm 13 = calibration  -2,-1,0,1,2 Etc
//   parm 12 = Time elapse auto report 0 = never, 1 = .5 hr, 2 = 1 hr (default), etc 
//   parm 11 Trigger autoreport if room temp different from last report  
//          0 = never, 1 = 1 deg F,  2 = 2 degree F,  3 = 3,  4 = 4 deg F (default) 
  //    if(settings.RptTime > 16){settings.RptTime = 16} // Every 8 hours is max
    log.debug "Setting Configuration cal=${settings.cal}, RptTime= ${settings.RptTime}, RptDelta = ${settings.RptDelta}"   //des
	try
    {
     delayBetween([
        zwave.configurationV1.configurationSet(parameterNumber: 13, size: 1, scaledConfigurationValue: settings.cal ).format(),
        zwave.configurationV1.configurationSet(parameterNumber: 12, size: 1, scaledConfigurationValue: settings.RptTime ).format(),
        zwave.configurationV1.configurationSet(parameterNumber: 11, size: 1, scaledConfigurationValue: settings.RptDelta ).format(),
  	    // Get reports and update value tiles
        zwave.configurationV1.configurationGet(parameterNumber: 13).format(), 
        zwave.configurationV1.configurationGet(parameterNumber: 12).format(),
        zwave.configurationV1.configurationGet(parameterNumber: 11).format()
    ], 2300)
    }
  catch (e)
  { 
      log.error "something went wrong in configure: $e" 
  }
}
// DES modified Command Implementation polling
def poll() {
	// DES  add poll of calibration parameter 13 
    delayBetween([
		zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
		zwave.thermostatModeV2.thermostatModeGet().format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
		zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format(),
        // DES update the configurations tiles
        zwave.configurationV1.configurationGet(parameterNumber: 13).format(),
		zwave.configurationV1.configurationGet(parameterNumber: 12).format(),
		zwave.configurationV1.configurationGet(parameterNumber: 11).format(),
        // DES get the battery status
        zwave.batteryV1.batteryGet().format()
	], 2300)
}

def parse(String description)
{
   // DES added 0x70: COMMAND_CLASS_CONFIGURATION so this will process configuration reports
   // DES added 0x80: COMMAND_CLASS_BATTERY so this will process battery reports
	def map = createEvent(zwaveEvent(zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3,  0x70: 1, 0x80: 1]))) // DES added 0x70, 1
	if (!map) {
		return null
	}

	def result = [map]
	if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) {
		def map2 = [
			name: "thermostatSetpoint",
			unit: getTemperatureScale()
		]
		if (map.name == "thermostatMode") {
			state.lastTriedMode = map.value
			if (map.value == "cool") {
				map2.value = device.latestValue("coolingSetpoint")
				log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}"
			}
			else {
				map2.value = device.latestValue("heatingSetpoint")
				log.info "THERMOSTAT, latest heating setpoint = ${map2.value}"
			}
		}
		else {
			def mode = device.latestValue("thermostatMode")
			log.info "THERMOSTAT, latest mode = ${mode}"
			if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) {
				map2.value = map.value
				map2.unit = map.unit
			}
		}
		if (map2.value != null) {
			log.debug "THERMOSTAT, adding setpoint event: $map"
			result << createEvent(map2)
		}
	} else if (map.name == "thermostatFanMode" && map.isStateChange) {
		state.lastTriedFanMode = map.value
	}
	log.debug "Parse returned $result"
	result
}

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

def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd)
{
	def map = [:]
	if (cmd.sensorType == 1) {
		map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision)
		map.unit = getTemperatureScale()
		map.name = "temperature"
	} else if (cmd.sensorType == 5) {
		map.value = cmd.scaledSensorValue
		map.unit = "%"
		map.name = "humidity"
	}
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd)
{
	def map = [:]
	switch (cmd.operatingState) {
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE:
			map.value = "idle"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING:
			map.value = "heating"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_COOLING:
			map.value = "cooling"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY:
			map.value = "fan only"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT:
			map.value = "pending heat"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL:
			map.value = "pending cool"
			break
		case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER:
			map.value = "vent economizer"
			break
	}
	map.name = "thermostatOperatingState"
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) {
	def map = [name: "thermostatFanState"]
	switch (cmd.fanOperatingState) {
		case 0:
			map.value = "idle"
			break
		case 1:
			map.value = "running"
			break
		case 2:
			map.value = "running high"
			break
	}
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) {
	def map = [:]
	switch (cmd.mode) {
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF:
			map.value = "off"
			break
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
			map.value = "heat"
			break
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT:
			map.value = "emergencyHeat"
			break
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL:
			map.value = "cool"
			break
		case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO:
			map.value = "auto"
			break
	}
	map.name = "thermostatMode"
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) {
	def map = [:]
	switch (cmd.fanMode) {
		case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW:
			map.value = "fanAuto"
			break
		case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW:
			map.value = "fanOn"
			break
		case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION:
			map.value = "fanCirculate"
			break
	}
	map.name = "thermostatFanMode"
	map.displayed = false
	map
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
	def supportedModes = ""
	if(cmd.off) { supportedModes += "off " }
	if(cmd.heat) { supportedModes += "heat " }
	if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergencyHeat " }
	if(cmd.cool) { supportedModes += "cool " }
	if(cmd.auto) { supportedModes += "auto " }

	state.supportedModes = supportedModes
}

def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) {
	def supportedFanModes = ""
	if(cmd.auto) { supportedFanModes += "fanAuto " }
	if(cmd.low) { supportedFanModes += "fanOn " }
	if(cmd.circulation) { supportedFanModes += "fanCirculate " }

	state.supportedFanModes = supportedFanModes
}

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

def zwaveEvent(physicalgraph.zwave.Command cmd) {
	log.warn "Unexpected zwave command $cmd"
}

// Command Implementations
def quickSetHeat(degrees) {
	setHeatingSetpoint(degrees, 1000)
}

def setHeatingSetpoint(degrees, delay = 30000) {
	setHeatingSetpoint(degrees.toDouble(), delay)
}

def setHeatingSetpoint(Double degrees, Integer delay = 30000) {
	log.trace "setHeatingSetpoint($degrees, $delay)"
	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
    }

	delayBetween([
		zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(),
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()
	], delay)
}

def quickSetCool(degrees) {
	setCoolingSetpoint(degrees, 1000)
}

def setCoolingSetpoint(degrees, delay = 30000) {
	setCoolingSetpoint(degrees.toDouble(), delay)
}

def setCoolingSetpoint(Double degrees, Integer delay = 30000) {
    log.trace "setCoolingSetpoint($degrees, $delay)"
	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
    }

	delayBetween([
		zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p,  scaledValue: convertedDegrees).format(),
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()
	], delay)
}

def modes() {
	["off", "heat", "cool", "auto", "emergencyHeat"]
}

def switchMode() {
	def currentMode = device.currentState("thermostatMode")?.value
	def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off"
	def supportedModes = getDataByName("supportedModes")
	def modeOrder = modes()
	def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
	def nextMode = next(lastTriedMode)
	if (supportedModes?.contains(currentMode)) {
		while (!supportedModes.contains(nextMode) && nextMode != "off") {
			nextMode = next(nextMode)
		}
	}
	state.lastTriedMode = nextMode
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	], 1000)
}

def switchToMode(nextMode) {
	def supportedModes = getDataByName("supportedModes")
	if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
	if (nextMode in modes()) {
		state.lastTriedMode = nextMode
		"$nextMode"()
	} else {
		log.debug("no mode method '$nextMode'")
	}
}

def switchFanMode() {
	def currentMode = device.currentState("thermostatFanMode")?.value
	def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off"
	def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn"
	def modeOrder = ["fanAuto", "fanCirculate", "fanOn"]
	def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
	def nextMode = next(lastTriedMode)
	while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") {
		nextMode = next(nextMode)
	}
	switchToFanMode(nextMode)
}

def switchToFanMode(nextMode) {
	def supportedFanModes = getDataByName("supportedFanModes")
	if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"

	def returnCommand
	if (nextMode == "fanAuto") {
		returnCommand = fanAuto()
	} else if (nextMode == "fanOn") {
		returnCommand = fanOn()
	} else if (nextMode == "fanCirculate") {
		returnCommand = fanCirculate()
	} else {
		log.debug("no fan mode '$nextMode'")
	}
	if(returnCommand) state.lastTriedFanMode = nextMode
	returnCommand
}

def getDataByName(String name) {
	state[name] ?: device.getDataValue(name)
}

def getModeMap() { [
	"off": 0,
	"heat": 1,
	"cool": 2,
	"auto": 3,
	"emergencyHeat": 4
]}

def setThermostatMode(String value) {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	], standardDelay)
}

def getFanModeMap() { [
	"auto": 0,
	"on": 1,
	"circulate": 6
]}

def setThermostatFanMode(String value) {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	], standardDelay)
}

def off() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	], standardDelay)
}

def heat() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	], standardDelay)
}

def emergencyHeat() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 4).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	], standardDelay)
}

def cool() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	], standardDelay)
}

def auto() {
	delayBetween([
		zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(),
		zwave.thermostatModeV2.thermostatModeGet().format()
	], standardDelay)
}

def fanOn() {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	], standardDelay)
}

def fanAuto() {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	], standardDelay)
}

def fanCirculate() {
	delayBetween([
		zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(),
		zwave.thermostatFanModeV3.thermostatFanModeGet().format()
	], standardDelay)
}

private getStandardDelay() {
	1000
}

Thanks for your work on this Dennis. Looks great. I have two of these and neither of these “AUTO report” wither according to my time interval setting or the room temperature difference. I have to manually refresh each one to pull down the current thermostat temp. Are you experiencing this as well?

This looks great and saves me from modifying it myself. Love the temperature adjustment change from the slider bar. I’d eventually like to add configuration parameters for “Swing” and “Differential”

Before that I have a problem. I have copied your code from above and published to my Hub. I see the updated UI, but not the configuration paramters on the control screen or the edit screens. Any ideas?

Firmware Version: 000.014.00026
Hardware Version: hub v2
iOS Version 2.0.7 (1220)

Here are a couple of screenshots from my iPhone


If I understand the code correctly I should see a couple of value outputs here showing the parameter values.


Shouldn’t I see a couple of inputs for cal, RptTime, RptDelta?

Thanks for your work and let me know if I can help in any way.

  • Scott

First to Tim. My Thermostat does report every 30 minutes when I set that parameter to 1. I check the ‘recently’ tab to verify. As to the temperature difference - not sure. At one time it was reporting every 1 degree as set, but it looks like that may not have happened last night. Perhaps the algorithm in the thermostat does not do the delta temperature update if it has done a time update. I will have to investigate that more and report back. I will probably add some log.trace to see what is actually happening. Update. I just looked at the recently tab again. That’s weird, the first look it only showed the time update events only, and now it shows the history from all of last night and it is reporting temperature every 30 minutes and also every change of 1 degree. Do not know why the Recently tab did not show that the first time I looked. The whole Smart Things is a lot smarter than I am I guess

To Scott. Where did you get the code for the thermostat device handler? I have not published my code so it is only available in this post at this time. The device handler you are using is not the one I developed for the ZTS-110. I did not add the up down icons to change the set point. Try again by creating a new handler. Log in, ( https://graph.api.smartthings.com/login/auth ) go to my device handlers, click the add new, proceed to the code section, copy and paste my code, save it, publish it for your use, and then install it to location for your thermostat. Then when you edit the device in the app, you should see the inputs as you suspected and instead of the up down icons you will see the value tiles for the settings in the main screen.

Scott: I think you used the code that I posted and I haven’t implemented Dennis’s configuration code into it yet. If you want to change the config settings you need to use his code however his uses sliders instead of the up/down buttons.
Dennis: Thanks for for the info regarding auto reporting. Mine isn’t working. I’ll try a full reset on the thermostat then use your code to set the parameters.

Error: ID10T DOH!!!

You miss one step and wonder why it doesn’t work. As you both have probably suspected I crossed the streams. I’ve been playing with both sets of code Tim’s “ZTS-110” and Dennis’ “ZTS-110 Thermostat”. Turns out I published both sets of code, but skipped / missed the device edit to select appropriate “Type”.

When I looked I had “ZTS-110” selected which explains UI differences and why the modifications I was making were not being picked up. Guess I should have looked a little closer.

Thanks,
Scott

So I figured out why my ZTS-110 wasn’t auto reporting…after reading the manual it turns out that it auto reports on Association group 3 and when devices are paired with ST Hub it only associates with group 1. Anyways I was able to send this configuration command to the thermostats to associate them to group 3 as well so now all the auto reports are coming in nicely. Hope this helps someone else.

“zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()”

Tim, I have to admit I have no idea what Association groups are. I did nothing in my code and nothing unusual when I first included the ZTS 110 in my network. I have the newest hub. So why does my thermostat auto report and yours did not until you did the association set? That looks like a command to the hub?
I notice in the template I used for the handler in the configure section there was this command which I did not change:

zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()

I suppose you just added the additional command in the configure section?
If some other device in my network set the association group 3 - would that do it for the thermostat?

This whole smart things is still dumb things to me. Perhaps I should not dive into what would take a few months to learn.

Do you have a link I can go to learn about association groups?

And finally, I like the up down buttons. Where can I find your code (if you care to share)

Dennis