diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index 5caf602..e7205e1 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -16,7 +16,6 @@ metadata { capability "Actuator" capability "Temperature Measurement" capability "Thermostat" - capability "Configuration" capability "Refresh" capability "Sensor" capability "Health Check" @@ -25,209 +24,175 @@ metadata { command "switchMode" command "switchFanMode" - command "quickSetCool" - command "quickSetHeat" + command "lowerHeatingSetpoint" + command "raiseHeatingSetpoint" + command "lowerCoolSetpoint" + command "raiseCoolSetpoint" fingerprint deviceId: "0x08" fingerprint inClusters: "0x43,0x40,0x44,0x31" fingerprint mfr:"0039", prod:"0011", model:"0001", deviceJoinName: "Honeywell Z-Wave Thermostat" } - // 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 "auto" : "command: 4403, payload: 00" // "fanAuto" - status "on" : "command: 4403, payload: 01" // "fanOn" - status "circulate" : "command: 4403, payload: 06" // "fanCirculate - - 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" - } - tiles { - // Using standardTile instead of valueTile as it renders the icon better - standardTile("temperature", "device.temperature", width: 2, height: 2) { - state("temperature", label:'${currentValue}°', icon: "st.thermostat.ac.air-conditioning", - 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"] - ] - ) + multiAttributeTile(name:"temperature", type:"generic", width:3, height:2, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal", + backgroundColors:[ + // Celsius + [value: 0, color: "#153591"], + [value: 7, color: "#1e9cbb"], + [value: 15, color: "#90d2a7"], + [value: 23, color: "#44b621"], + [value: 28, color: "#f1d801"], + [value: 35, color: "#d04e00"], + [value: 37, color: "#bc2323"], + // Fahrenheit + [value: 40, 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", action:"switchMode", nextState:"to_heat", icon: "st.thermostat.heating-cooling-off" - state "heat", action:"switchMode", nextState:"to_cool", icon: "st.thermostat.heat" + standardTile("mode", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { + state "off", action:"switchMode", nextState:"...", icon: "st.thermostat.heating-cooling-off" + state "heat", action:"switchMode", nextState:"...", icon: "st.thermostat.heat" state "cool", action:"switchMode", nextState:"...", icon: "st.thermostat.cool" state "auto", action:"switchMode", nextState:"...", icon: "st.thermostat.auto" state "emergency heat", action:"switchMode", nextState:"...", icon: "st.thermostat.emergency-heat" - state "to_heat", action:"switchMode", nextState:"to_cool", icon: "st.thermostat.heat" - state "to_cool", action:"switchMode", nextState:"...", icon: "st.thermostat.cool" - state "...", label: "...", action:"off", nextState:"off" + state "...", label: "Updating...",nextState:"...", backgroundColor:"#ffffff" } - standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { - state "auto", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-auto" // "fanAuto" - state "on", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-on" // "fanOn" - state "circulate", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-circulate" // "fanCirculate" - state "...", label: "...", nextState:"..." + standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { + state "auto", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-auto" + state "on", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-on" + state "circulate", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-circulate" + state "...", label: "Updating...", nextState:"...", backgroundColor:"#ffffff" } - controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00" + standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left" } - valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { - state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff" + valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff" } - controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb" + standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right" } - valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff" + standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left" } - standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff" + } + standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right" + } + valueTile("thermostatOperatingState", "device.thermostatOperatingState", width: 2, height:1, decoration: "flat") { + state "thermostatOperatingState", label:'${currentValue}', backgroundColor:"#ffffff" + } + standardTile("refresh", "device.thermostatMode", width:2, height:1, inactiveLabel: false, decoration: "flat") { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } main "temperature" - details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh"]) + details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint", + "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode", "thermostatOperatingState", "refresh"]) } } -def installed(){ - sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format())) - initialize() +def installed() { + // Configure device + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()) + cmds << new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + sendHubCommand(cmds) + runIn(3, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding } -def updated(){ - initialize() +def updated() { + // If not set update ManufacturerSpecific data + if (!getDataValue("manufacturer")) { + sendHubCommand(new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())) + runIn(2, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding + } else { + initialize() + } } def initialize() { // Device-Watch simply pings if no device events received for 32min(checkInterval) sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) unschedule() - runEvery5Minutes("refresh") - refresh() + if (getDataValue("manufacturer") != "Honeywell") { + runEvery5Minutes("poll") // This is not necessary for Honeywell Z-wave, but could be for other Z-wave thermostats + } + poll() } def parse(String description) { - def map = createEvent(zwaveEvent(zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3]))) - if (!map) { - return null + def result = null + if (description == "updated") { + } else { + def zwcmd = zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3]) + if (zwcmd) { + result = zwaveEvent(zwcmd) + } else { + log.debug "$device.displayName couldn't parse $description" + } } - - 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 - map.data = [supportedThermostatModes:state.supportedThermostatModes] - 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 - map.data = [supportedThermostatFanModes: state.supportedThermostatFanModes] + if (!result) { + return [] } - log.debug "Parse returned $result" - result + return [result] } // Event Generation -def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) -{ +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 + def setpoint = getTempInLocalScale(cmd.scaledValue, cmdScale) + def unit = getTemperatureScale() switch (cmd.setpointType) { case 1: - map.name = "heatingSetpoint" + sendEvent(name: "heatingSetpoint", value: setpoint, unit: unit, displayed: false) + updateThermostatSetpoint("heatingSetpoint", setpoint) break; case 2: - map.name = "coolingSetpoint" + sendEvent(name: "coolingSetpoint", value: setpoint, unit: unit, displayed: false) + updateThermostatSetpoint("coolingSetpoint", setpoint) break; default: - return [:] + log.debug "unknown setpointType $cmd.setpointType" + return } // So we can respond with same format state.size = cmd.size state.scale = cmd.scale state.precision = cmd.precision - map + // Make sure return value is not result from above expresion + return 0 } -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) -{ +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.value = getTempInLocalScale(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C") map.unit = getTemperatureScale() map.name = "temperature" + updateThermostatSetpoint(null, null) } else if (cmd.sensorType == 5) { map.value = cmd.scaledSensorValue map.unit = "%" map.name = "humidity" } - map + sendEvent(map) } -def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) -{ - def map = [:] +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) { + def map = [name: "thermostatOperatingState"] switch (cmd.operatingState) { case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: map.value = "idle" @@ -251,8 +216,9 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.Thermosta map.value = "vent economizer" break } - map.name = "thermostatOperatingState" - map + // Makes sure we have the correct thermostat mode + sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { @@ -268,11 +234,11 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanSt map.value = "running high" break } - map + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { - def map = [:] + def map = [name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes]] switch (cmd.mode) { case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: map.value = "off" @@ -290,26 +256,24 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeRepor map.value = "auto" break } - map.name = "thermostatMode" - map + sendEvent(map) + updateThermostatSetpoint(null, null) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { - def map = [:] + def map = [name: "thermostatFanMode", data:[supportedThermostatFanModes: state.supportedFanModes]] switch (cmd.fanMode) { case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: - map.value = "auto" // "fanAuto" + map.value = "auto" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: - map.value = "on" // "fanOn" + map.value = "on" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: - map.value = "circulate" // "fanCirculate" + map.value = "circulate" break } - map.name = "thermostatFanMode" - map.displayed = false - map + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { @@ -320,24 +284,34 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSuppo if(cmd.auto) { supportedModes << "auto" } if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" } - state.supportedThermostatModes = supportedModes + state.supportedModes = supportedModes sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false) - return [:] } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { def supportedFanModes = [] - if(cmd.auto) { supportedFanModes << "auto" } // "fanAuto " - if(cmd.circulation) { supportedFanModes << "circulate" } // "fanCirculate" - if(cmd.low) { supportedFanModes << "on" } // "fanOn" + if(cmd.auto) { supportedFanModes << "auto" } + if(cmd.circulation) { supportedFanModes << "circulate" } + if(cmd.low) { supportedFanModes << "on" } - state.supportedThermostatFanModes = supportedFanModes + state.supportedFanModes = supportedFanModes sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false) - return [:] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + if (cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + if (cmd.productTypeId) { + updateDataValue("productTypeId", cmd.productTypeId.toString()) + } + if (cmd.productId) { + updateDataValue("productId", cmd.productId.toString()) + } } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - log.debug "Zwave event received: $cmd" + log.debug "Zwave BasicReport: $cmd" } def zwaveEvent(physicalgraph.zwave.Command cmd) { @@ -346,6 +320,16 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { // Command Implementations def refresh() { + // Only allow refresh every 2 minutes to prevent flooding the Zwave network + def timeNow = now() + if (!state.refreshTriggeredAt || (2 * 60 * 1000 < (timeNow - state.refreshTriggeredAt))) { + state.refreshTriggeredAt = timeNow + // use runIn with overwrite to prevent multiple DTH instances run before state.refreshTriggeredAt has been saved + runIn(2, "poll", [overwrite: true]) + } +} + +def poll() { def cmds = [] cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format()) @@ -358,64 +342,152 @@ def refresh() { sendHubCommand(cmds) } -def quickSetHeat(degrees) { - setHeatingSetpoint(degrees, 1000) +def raiseHeatingSetpoint() { + alterSetpoint(true, "heatingSetpoint") } -def setHeatingSetpoint(degrees, delay = 30000) { - setHeatingSetpoint(degrees.toDouble(), delay) +def lowerHeatingSetpoint() { + alterSetpoint(false, "heatingSetpoint") } -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 raiseCoolSetpoint() { + alterSetpoint(true, "coolingSetpoint") } -def quickSetCool(degrees) { - setCoolingSetpoint(degrees, 1000) +def lowerCoolSetpoint() { + alterSetpoint(false, "coolingSetpoint") } -def setCoolingSetpoint(degrees, delay = 30000) { - setCoolingSetpoint(degrees.toDouble(), delay) +// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false +def alterSetpoint(raise, setpoint) { + def locationScale = getTemperatureScale() + def deviceScale = (state.scale == 1) ? "F" : "C" + def heatingSetpoint = getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = getTempInLocalScale("coolingSetpoint") + def targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint + def delta = (locationScale == "F") ? 1 : 0.5 + targetValue += raise ? delta : - delta + + def data = enforceSetpointLimits(setpoint, [targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) + // update UI without waiting for the device to respond, this to give user a smoother UI experience + // also, as runIn's have to overwrite and user can change heating/cooling setpoint separately separate runIn's have to be used + if (data.targetHeatingSetpoint) { + sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, deviceScale), + unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) + } + if (data.targetCoolingSetpoint) { + sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale), + unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) + } + if (data.targetHeatingSetpoint && data.targetCoolingSetpoint) { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + } else if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { + runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true]) + } } -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 updateHeatingSetpoint(data) { + updateSetpoints(data) +} - def convertedDegrees - if (locationScale == "C" && deviceScaleString == "F") { - convertedDegrees = celsiusToFahrenheit(degrees) - } else if (locationScale == "F" && deviceScaleString == "C") { - convertedDegrees = fahrenheitToCelsius(degrees) - } else { - convertedDegrees = degrees - } +def updateCoolingSetpoint(data) { + updateSetpoints(data) +} - delayBetween([ - zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format() - ], delay) +def enforceSetpointLimits(setpoint, data) { + def locationScale = getTemperatureScale() + def minSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(40, "F") : getTempInDeviceScale(50, "F") + def maxSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(90, "F") : getTempInDeviceScale(99, "F") + def deadband = (state.scale == 1) ? 3 : 2 // 3°F, 2°C + def targetValue = getTempInDeviceScale(data.targetValue, locationScale) + def heatingSetpoint = null + def coolingSetpoint = null + // Enforce min/mix for setpoints + if (targetValue > maxSetpoint) { + targetValue = maxSetpoint + } else if (targetValue < minSetpoint) { + targetValue = minSetpoint + } + // Enforce 3 degrees F deadband between setpoints + if (setpoint == "heatingSetpoint") { + heatingSetpoint = targetValue + coolingSetpoint = (heatingSetpoint + deadband > getTempInDeviceScale(data.coolingSetpoint, locationScale)) ? heatingSetpoint + deadband : null + } + if (setpoint == "coolingSetpoint") { + coolingSetpoint = targetValue + heatingSetpoint = (coolingSetpoint - deadband < getTempInDeviceScale(data.heatingSetpoint, locationScale)) ? coolingSetpoint - deadband : null + } + return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint] +} + +def setHeatingSetpoint(degrees) { + if (degrees) { + state.heatingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) + } +} + +def setCoolingSetpoint(degrees) { + if (degrees) { + state.coolingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) + } +} + +def updateSetpoints() { + def deviceScale = (state.scale == 1) ? "F" : "C" + def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null] + def heatingSetpoint = getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = getTempInLocalScale("coolingSetpoint") + if (state.heatingSetpoint) { + data = enforceSetpointLimits("heatingSetpoint", [targetValue: state.heatingSetpoint, + heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) + } + if (state.coolingSetpoint) { + heatingSetpoint = data.targetHeatingSetpoint ? getTempInLocalScale(data.targetHeatingSetpoint, deviceScale) : heatingSetpoint + coolingSetpoint = data.targetCoolingSetpoint ? getTempInLocalScale(data.targetCoolingSetpoint, deviceScale) : coolingSetpoint + data = enforceSetpointLimits("coolingSetpoint", [targetValue: state.coolingSetpoint, + heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) + data.targetHeatingSetpoint = data.targetHeatingSetpoint ?: heatingSetpoint + } + state.heatingSetpoint = null + state.coolingSetpoint = null + updateSetpoints(data) +} + +def updateSetpoints(data) { + def cmds = [] + if (data.targetHeatingSetpoint) { + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( + setpointType: 1, scale: state.scale, precision: state.precision, scaledValue: data.targetHeatingSetpoint).format()) + } + if (data.targetCoolingSetpoint) { + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( + setpointType: 2, scale: state.scale, precision: state.precision, scaledValue: data.targetCoolingSetpoint).format()) + } + sendHubCommand(cmds) +} + +// thermostatSetpoint is not displayed by any tile as it can't be predictable calculated due to +// the device's quirkiness but it is defined by the capability so it must be set, set it to the most likely value +def updateThermostatSetpoint(setpoint, value) { + def scale = getTemperatureScale() + def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint") + def mode = device.currentValue("thermostatMode") + def thermostatSetpoint = heatingSetpoint // corresponds to (mode == "heat" || mode == "emergency heat") + if (mode == "cool") { + thermostatSetpoint = coolingSetpoint + } else if (mode == "auto" || mode == "off") { + // Set thermostatSetpoint to the setpoint closest to the current temperature + def currentTemperature = getTempInLocalScale("temperature") + if (currentTemperature > (heatingSetpoint + coolingSetpoint)/2) { + thermostatSetpoint = coolingSetpoint + } + } + sendEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) } /** @@ -423,76 +495,74 @@ def setCoolingSetpoint(Double degrees, Integer delay = 30000) { * */ def ping() { log.debug "ping() called" - poll() -} - -def modes() { - return state.supportedThermostatModes + // Just get Operating State there's no need to flood more commands + sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())) } def switchMode() { - def currentMode = device.currentState("thermostatMode")?.value - def lastTriedMode = state.lastTriedMode ?: currentMode ?: ["off"] - def supportedModes = getDataByName("supportedThermostatModes") - 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) - } + def currentMode = device.currentValue("thermostatMode") + def supportedModes = state.supportedModes + if (supportedModes) { + def next = { supportedModes[supportedModes.indexOf(it) + 1] ?: supportedModes[0] } + def nextMode = next(currentMode) + runIn(2, "setThermostatMode", [data: [nextMode: nextMode], overwrite: true]) + } else { + log.warn "supportedModes not defined" + getSupportedModes() } - state.lastTriedMode = nextMode - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], 1000) } def switchToMode(nextMode) { - def supportedModes = getDataByName("supportedThermostatModes") - if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - if (nextMode in modes()) { - state.lastTriedMode = nextMode - "$nextMode"() + def supportedModes = state.supportedModes + if (supportedModes) { + if (supportedModes.contains(nextMode)) { + runIn(2, "setThermostatMode", [data: [nextMode: nextMode], overwrite: true]) + } else { + log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}") + } } else { - log.debug("no mode method '$nextMode'") + log.warn "supportedModes not defined" + getSupportedModes() } } +def getSupportedModes() { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) + sendHubCommand(cmds) +} + def switchFanMode() { - def currentMode = device.currentState("thermostatFanMode")?.value - def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: ["off"] - def supportedModes = getDataByName("supportedThermostatFanModes") ?: ["auto", "on"] - def modeOrder = state.supportedThermostatFanModes - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - while (!supportedModes?.contains(nextMode) && nextMode != "auto") { // "fanAuto" - nextMode = next(nextMode) + def currentMode = device.currentValue("thermostatFanMode") + def supportedFanModes = state.supportedFanModes + if (supportedFanModes) { + def next = { supportedFanModes[supportedFanModes.indexOf(it) + 1] ?: supportedFanModes[0] } + def nextMode = next(currentMode) + runIn(2, "setThermostatFanMode", [data: [nextMode: nextMode], overwrite: true]) + } else { + log.warn "supportedFanModes not defined" + getSupportedFanModes() } - switchToFanMode(nextMode) } def switchToFanMode(nextMode) { - def supportedFanModes = getDataByName("supportedThermostatFanModes") - if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - - def returnCommand - if (nextMode == "auto") { // "fanAuto" - returnCommand = fanAuto() - } else if (nextMode == "on") { // "fanOn" - returnCommand = fanOn() - } else if (nextMode == "circulate") { // "fanCirculate" - returnCommand = fanCirculate() + def supportedFanModes = state.supportedFanModes + if (supportedFanModes) { + if (supportedFanModes.contains(nextMode)) { + runIn(2, "setThermostatFanMode", [data: [nextMode: nextMode], overwrite: true]) + } else { + log.debug("FanMode $nextMode is not supported by ${device.displayName}") + } } else { - log.debug("no fan mode '$nextMode'") + log.warn "supportedFanModes not defined" + getSupportedFanModes() } - if(returnCommand) state.lastTriedFanMode = nextMode - returnCommand } -def getDataByName(String name) { - state[name] ?: device.getDataValue(name) +def getSupportedFanModes() { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format()) + sendHubCommand(cmds) } def getModeMap() { [ @@ -504,10 +574,14 @@ def getModeMap() { [ ]} def setThermostatMode(String value) { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode(value) +} + +def setThermostatMode(data) { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]).format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) + sendHubCommand(cmds) } def getFanModeMap() { [ @@ -517,69 +591,83 @@ def getFanModeMap() { [ ]} def setThermostatFanMode(String value) { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode(value) +} + +def setThermostatFanMode(data) { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]).format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format()) + sendHubCommand(cmds) } def off() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("off") } def heat() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("heat") } def emergencyHeat() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 4).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("emergency heat") } def cool() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("cool") } def auto() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("auto") } def fanOn() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("on") } def fanAuto() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("auto") } def fanCirculate() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("circulate") } -private getStandardDelay() { - 1000 +// Get stored temperature from currentState in current local scale +def getTempInLocalScale(state) { + def temp = device.currentState(state) + if (temp && temp.value && temp.unit) { + return getTempInLocalScale(temp.value.toBigDecimal(), temp.unit) + } + return 0 } +// get/convert temperature to current local scale +def getTempInLocalScale(temp, scale) { + if (temp && scale) { + def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble() + return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp)) + } + return 0 +} + +def getTempInDeviceScale(state) { + def temp = device.currentState(state) + if (temp && temp.value && temp.unit) { + return getTempInDeviceScale(temp.value.toBigDecimal(), temp.unit) + } + return 0 +} + +def getTempInDeviceScale(temp, scale) { + if (temp && scale) { + def deviceScale = (state.scale == 1) ? "F" : "C" + return (deviceScale == scale) ? temp : + (deviceScale == "F" ? celsiusToFahrenheit(temp) : roundC(fahrenheitToCelsius(temp))) + } + return 0 +} + +def roundC (tempC) { + return (Math.round(tempC.toDouble() * 2))/2 +}