/** * 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 (Config to set reporting)", namespace: "Thermostat Config Setup", author: "Tim") { 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 //raw description (DES): 0 0 0x1001 0 0 0 4 0x25 0x27 0x86 0x72 //raw dscription (Tim): 0 0 0x0806 0 0 0 d 0x20 0x31 0x40 0x42 0x43 0x44 0x45 0x47 0x70 0x72 0x81 0x85 0x86 //fingerprint deviceId:"0x0806", inClusters:"0x20, 0x31, 0x40, 0x42, 0x43, 0x44, 0x45, 0x47, 0x70, 0x72, 0x81, 0x85, 0x86" } // 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(hubitat.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(hubitat.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 - 246 } 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.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(), zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format(), //Tim's Code End ], 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(), // 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(hubitat.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(hubitat.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(hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) { def map = [:] switch (cmd.operatingState) { case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: map.value = "idle" break case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING: map.value = "heating" break case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_COOLING: map.value = "cooling" break case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY: map.value = "fan only" break case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT: map.value = "pending heat" break case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL: map.value = "pending cool" break case hubitat.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER: map.value = "vent economizer" break } map.name = "thermostatOperatingState" map } def zwaveEvent(hubitat.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(hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { def map = [:] switch (cmd.mode) { case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: map.value = "off" break case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT: map.value = "heat" break case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT: map.value = "emergencyHeat" break case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: map.value = "cool" break case hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO: map.value = "auto" break } map.name = "thermostatMode" map } def zwaveEvent(hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { def map = [:] switch (cmd.fanMode) { case hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: map.value = "fanAuto" break case hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: map.value = "fanOn" break case hubitat.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: map.value = "fanCirculate" break } map.name = "thermostatFanMode" map.displayed = false map } def zwaveEvent(hubitat.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(hubitat.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(hubitat.zwave.commands.basicv1.BasicReport cmd) { log.debug "Zwave event received: $cmd" } def zwaveEvent(hubitat.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 }