Here is a device driver for Z-Wave Thermostats that I ported from SmartThings to Hubitat. This is based on the basic enhanced driver that RBoy released out into the wild (one of his free apps, not paid).
I've been holding off on posting this until the MultiInstanceCmdEncap issues were sorted out, which happened in the latest release (1.1.6). That is to say, your hub must be running v1.1.6 or newer for the humidity reporting to work.
Note that the CT-100 does not report humidity very often unless polled. I'm using RM to poll the thermostat every few minutes to ensure the humidity is up to date. You can use either poll or refresh to do this, both have the same effect.
/**
* Copyright 2015 SmartThings
*
* 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 {
definition (name: "Enhanced Z-Wave Thermostat", namespace: "rboy", author: "RBoy") {
capability "Actuator"
capability "Temperature Measurement"
capability "Relative Humidity Measurement"
capability "Thermostat"
capability "Configuration"
capability "Polling"
capability "Sensor"
attribute "thermostatFanState", "string"
command "switchMode"
command "switchFanMode"
command "quickSetCool"
command "quickSetHeat"
// CUSTOMIZATION
capability "Refresh"
capability "Battery"
// END CUSTOMIZATION
fingerprint deviceId: "0x08"
fingerprint inClusters: "0x43,0x40,0x44,0x31"
}
}
def pollDevice() {
log.debug "pollDevice() - redirecting to poll()"
poll()
}
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: 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.sensormultilevelv2.SensorMultilevelReport cmd)
{
def map = [:]
map.displayed = true
map.isStateChange = true
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", unit: ""]
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 = "emergency heat"
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 += "emergency heat " }
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 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(),
getBattery(), // CUSTOMIZATION
setClock(), // CUSTOMIZATION
zwave.multiInstanceV1.multiInstanceCmdEncap(instance: 2).encapsulate(zwave.sensorMultilevelV2.sensorMultilevelGet()).format(), // CT-100/101 Customization for Humidity
], 2300)
}
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 configure() {
delayBetween([
zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
], 2300)
}
def modes() {
["off", "heat", "cool", "auto", "emergency heat"]
}
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,
"emergency heat": 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
}
// CUSTOMIZATIONS
def zwaveEvent(hubitat.zwave.commands.multiinstancev1.MultiInstanceCmdEncap cmd) {
log.trace ("multiinstancev1.MultiInstanceCmdEncap: command: ${cmd}")
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 2])
log.debug ("multiinstancev1.MultiInstanceCmdEncap: command from instance ${cmd.instance}: ${encapsulatedCommand}")
if (encapsulatedCommand) {
return zwaveEvent(encapsulatedCommand)
}
}
def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
def nowTime = new Date().time
state.lastBatteryGet = nowTime
def map = [ name: "battery", unit: "%" ]
map.displayed = true
map.isStateChange = true
if (cmd.batteryLevel == 0xFF || cmd.batteryLevel == 0) {
map.value = 1
map.descriptionText = "battery is low!"
} else {
map.value = cmd.batteryLevel
}
map
}
private getBattery() { //once every 24 hours
def nowTime = new Date().time
def ageInMinutes = state.lastBatteryGet ? (nowTime - state.lastBatteryGet)/60000 : 1440
log.debug "Battery report age: ${ageInMinutes} minutes"
if (ageInMinutes >= 1440) {
log.debug "Fetching fresh battery value"
zwave.batteryV1.batteryGet().format()
} else "delay 87"
}
private setClock() { // once a day
def nowTime = new Date().time
def ageInMinutes = state.lastClockSet ? (nowTime - state.lastClockSet)/60000 : 1440
log.debug "Clock set age: ${ageInMinutes} minutes"
if (ageInMinutes >= 1440) {
state.lastClockSet = nowTime
def nowCal = Calendar.getInstance(location.timeZone) // get current location timezone
log.debug "Setting clock to ${nowCal.getTime().format("EEE MMM dd yyyy HH:mm:ss z", location.timeZone)}"
sendEvent(name: "SetClock", value: "setting clock to ${nowCal.getTime().format("EEE MMM dd yyyy HH:mm:ss z", location.timeZone)}", displayed: true, isStateChange: true)
zwave.clockV1.clockSet(hour: nowCal.get(Calendar.HOUR_OF_DAY), minute: nowCal.get(Calendar.MINUTE), weekday: nowCal.get(Calendar.DAY_OF_WEEK)).format()
} else "delay 87"
}
def refresh() {
// Force a refresh
log.info "Requested a refresh"
state.lastBatteryGet = (new Date().time) - (1440 * 60000)
//state.lastClockSet = (new Date().time) - (1440 * 60000)
poll()
}