From 7aa7f619a8a53deee27ef523af5c0ffe5b5b25fb Mon Sep 17 00:00:00 2001 From: marstorp Date: Fri, 28 Jul 2017 17:54:49 -0700 Subject: [PATCH 1/8] ICP-1377 Z-Wave Thermostat incorrectly supports range Z-Wave Thermostat incorrectly supports range of 0-100 for temp slider Replaced slider UI with left/right arrow to change setpoints and made the change methods a bit more robust. Also replaced temperature tile with multi-attribute tile using thermometer icon. --- .../zwave-thermostat.groovy | 656 ++++++++++-------- 1 file changed, 365 insertions(+), 291 deletions(-) diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index 5caf602..0f7397b 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,177 @@ 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 "to_heat", action:"switchMode", nextState:"to_cool", icon: "st.secondary.secondary" + state "to_cool", action:"switchMode", nextState:"...", icon: "st.secondary.secondary" + state "...", label: "...",nextState:"...", icon: "st.secondary.secondary" } - standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, 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:"..." } - 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 [null] } - 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 +218,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 +236,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,12 +258,12 @@ 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" @@ -307,9 +275,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanMod map.value = "circulate" // "fanCirculate" break } - map.name = "thermostatFanMode" - map.displayed = false - map + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { @@ -320,24 +286,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 +322,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 +344,135 @@ 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") { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + } else if (setpoint == "coolingSetpoint") { + 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) { + def data = enforceSetpointLimits("heatingSetpoint", + [targetValue: getTempInDeviceScale(degrees.toDouble(), getTemperatureScale()), + heatingSetpoint: getTempInDeviceScale("heatingSetpoint"), coolingSetpoint: getTempInDeviceScale("coolingSetpoint")]) + updateSetpoints(data) + } +} + +def setCoolingSetpoint(degrees) { + if (degrees) { + def data = enforceSetpointLimits("coolingSetpoint", + [targetValue: getTempInDeviceScale(degrees.toDouble(), getTemperatureScale()), + heatingSetpoint: getTempInDeviceScale("heatingSetpoint"), coolingSetpoint: getTempInDeviceScale("coolingSetpoint")]) + 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 +480,75 @@ 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) + setThermostatFanMode(nextMode) + 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 +560,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 +577,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 +} From fadcf3426aa1c2cd27cd301eccb6499536281845 Mon Sep 17 00:00:00 2001 From: Ingvar Marstorp Date: Fri, 28 Jul 2017 18:13:21 -0700 Subject: [PATCH 2/8] Update zwave-thermostat.groovy --- .../smartthings/zwave-thermostat.src/zwave-thermostat.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index 0f7397b..55ab091 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -147,7 +147,7 @@ def parse(String description) } } if (!result) { - return [null] + return [] } return [result] } From e0a4f1c13e522716a4f51c9126f0820b37c7eacf Mon Sep 17 00:00:00 2001 From: Ingvar Marstorp Date: Fri, 28 Jul 2017 18:22:44 -0700 Subject: [PATCH 3/8] Update zwave-thermostat.groovy --- .../smartthings/zwave-thermostat.src/zwave-thermostat.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index 55ab091..0a8ab7a 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -258,7 +258,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeRepor map.value = "auto" break } - sendEvent(map) + sendEvent(map) updateThermostatSetpoint(null, null) } From e418f588979a14acfbd4ce9cc8d3a4698aa26a1e Mon Sep 17 00:00:00 2001 From: Ingvar Marstorp Date: Fri, 28 Jul 2017 18:25:37 -0700 Subject: [PATCH 4/8] Update zwave-thermostat.groovy --- .../smartthings/zwave-thermostat.src/zwave-thermostat.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index 0a8ab7a..f90ea6e 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -523,7 +523,6 @@ def switchFanMode() { if (supportedFanModes) { def next = { supportedFanModes[supportedFanModes.indexOf(it) + 1] ?: supportedFanModes[0] } def nextMode = next(currentMode) - setThermostatFanMode(nextMode) runIn(2, "setThermostatFanMode", [data: [nextMode: nextMode], overwrite: true]) } else { log.warn "supportedFanModes not defined" From 249825d25262f843afa4b3990aca1741916ed960 Mon Sep 17 00:00:00 2001 From: Ingvar Marstorp Date: Fri, 28 Jul 2017 18:56:11 -0700 Subject: [PATCH 5/8] Update zwave-thermostat.groovy --- .../smartthings/zwave-thermostat.src/zwave-thermostat.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index f90ea6e..7210764 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -375,11 +375,11 @@ def alterSetpoint(raise, setpoint) { // 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) + 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) + unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) } if (data.targetHeatingSetpoint && data.targetCoolingSetpoint) { runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) From 9c655e271fba98010e75fed2ba4945a043e3d747 Mon Sep 17 00:00:00 2001 From: Ingvar Marstorp Date: Mon, 31 Jul 2017 14:22:49 -0700 Subject: [PATCH 6/8] Update zwave-thermostat.groovy --- .../zwave-thermostat.src/zwave-thermostat.groovy | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index 7210764..d8e7cd7 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -65,15 +65,13 @@ metadata { 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.secondary.secondary" - state "to_cool", action:"switchMode", nextState:"...", icon: "st.secondary.secondary" - state "...", label: "...",nextState:"...", icon: "st.secondary.secondary" + state "...", label: "Updating...",nextState:"...", backgroundColor:"#ffffff" } standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, 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:"..." + 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" } standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left" From e0fe559014c1010fd1b1b1b80d9bc3066b02ad8c Mon Sep 17 00:00:00 2001 From: marstorp Date: Thu, 3 Aug 2017 11:50:57 -0700 Subject: [PATCH 7/8] PROB-1764, PROB-1767 CT100 Thermostat PROB-1764 Switching CT100 Thermostat from 'Cool' to 'Off' in SmartThings app causes Hub to become unresponsive PROB-1767 CT100 Not Responding to Routine Cooling Setpoint Command if Heating Setpoint included as well Removed action from 'Updating' state to prevent DTH sending more commands until response is received Changed how setpoints are updated, also added deadband Added thermostat operating state Changed humidity valueTile to standardTile --- .../ct100-thermostat.groovy | 425 +++++++++++------- 1 file changed, 272 insertions(+), 153 deletions(-) diff --git a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy index ad313d2..5a53439 100644 --- a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy +++ b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy @@ -53,21 +53,20 @@ metadata { } } standardTile("mode", "device.thermostatMode", width:2, height:2, 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" + 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.secondary.secondary" - state "to_cool", action:"switchMode", nextState:"...", icon: "st.secondary.secondary" - state "...", label: "...", action:"off", nextState:"off", icon: "st.secondary.secondary" + state "...", label: "Updating...",nextState:"...", backgroundColor:"#ffffff" } standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { - state "auto", action:"switchFanMode", icon: "st.thermostat.fan-auto" - state "on", action:"switchFanMode", icon: "st.thermostat.fan-on" - state "circulate", action:"switchFanMode", icon: "st.thermostat.fan-circulate" + 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" } - valueTile("humidity", "device.humidity", width:2, height:2, inactiveLabel: false, decoration: "flat") { + standardTile("humidity", "device.humidity", width:2, height:2, inactiveLabel: false, decoration: "flat") { state "humidity", label:'${currentValue}%', icon:"st.Weather.weather12" } standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { @@ -88,37 +87,43 @@ metadata { standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right" } - + standardTile("thermostatOperatingState", "device.thermostatOperatingState", width: 2, height:2, decoration: "flat") { + state "thermostatOperatingState", label:'${currentValue}', backgroundColor:"#ffffff" + } standardTile("refresh", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } main "temperature" - details(["temperature", "mode", "fanMode", "humidity", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint", "coolingSetpoint", "raiseCoolSetpoint", "refresh"]) + details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint", + "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode", "humidity", "thermostatOperatingState", "refresh"]) } } -def updated() { - // If not set update ManufacturerSpecific data - if (!getDataValue("manufacturer")) { - sendHubCommand(new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().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) - initialize() + runIn(3, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding +} + +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() { + unschedule() // 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]) // Poll device for additional data that will be updated by refresh tile - refresh() + poll() } def parse(String description) @@ -151,35 +156,43 @@ def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiInstanceCmdEncap } } -def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) -{ +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { def sendCmd = [] def unit = getTemperatureScale() def cmdScale = cmd.scale == 1 ? "F" : "C" def setpoint = getTempInLocalScale(cmd.scaledValue, cmdScale) + def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint") + def mode = device.currentValue("thermostatMode") + + // Save device scale, precision, scale as they are used when enforcing setpoint limits + if (cmd.setpointType == 1 || cmd.setpointType == 2) { + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision + } switch (cmd.setpointType) { - case 1: - //map1.name = "heatingSetpoint" - sendEvent(name: "heatingSetpoint", value: setpoint, unit: unit, displayed: false) - updateThermostatSetpoint("heatingSetpoint", setpoint) - // Enforce coolingSetpoint limits, as device doesn't - if (setpoint > getTempInLocalScale("coolingSetpoint")) { - sendCmd << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( - setpointType: 2, scale: cmd.scale, precision: cmd.precision, scaledValue: cmd.scaledValue).format()) - sendCmd << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) - sendHubCommand(sendCmd) + case 1: // "heatingSetpoint" + state.deviceHeatingSetpoint = cmd.scaledValue + if (state.targetHeatingSetpoint) { + state.targetHeatingSetpoint = null + sendEvent(name: "heatingSetpoint", value: setpoint, unit: getTemperatureScale()) + } else if (mode != "cool") { + // if mode is cool heatingSetpoint can't be changed on device, disregard update + // else update heatingSetpoint and enforce limits on coolingSetpoint + updateEnforceSetpointLimits("heatingSetpoint", setpoint) } break; - case 2: - //map1.name = "coolingSetpoint" - sendEvent(name: "coolingSetpoint", value: setpoint, unit: unit, displayed: false) - updateThermostatSetpoint("coolingSetpoint", setpoint) - // Enforce heatingSetpoint limits, as device doesn't - if (setpoint < getTempInLocalScale("heatingSetpoint")) { - sendCmd << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( - setpointType: 1, scale: cmd.scale, precision: cmd.precision, scaledValue: cmd.scaledValue).format()) - sendCmd << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) - sendHubCommand(sendCmd) + case 2: // "coolingSetpoint" + state.deviceCoolingSetpoint = cmd.scaledValue + if (state.targetCoolingSetpoint) { + state.targetCoolingSetpoint = null + sendEvent(name: "coolingSetpoint", value: setpoint, unit: getTemperatureScale()) + } else if (mode != "heat" || mode != "emergency heat") { + // if mode is heat or emergency heat coolingSetpoint can't be changed on device, disregard update + // else update coolingSetpoint and enforce limits on heatingSetpoint + updateEnforceSetpointLimits("coolingSetpoint", setpoint) } break; default: @@ -187,28 +200,6 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpo return } - // So we can respond with same format - state.size = cmd.size - state.scale = cmd.scale - state.precision = cmd.precision -} - -// 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 - } - // Just set to average of heating + cooling for mode off and auto - if (mode == "off" || mode == "auto") { - thermostatSetpoint = getTempInLocalScale((heatingSetpoint + coolingSetpoint)/2, scale) - } - sendEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: scale) } def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) { @@ -217,6 +208,7 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelR map.name = "temperature" map.unit = getTemperatureScale() map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C")) + updateThermostatSetpoint(null, null) } else if (cmd.sensorType == 5) { map.name = "humidity" map.unit = "%" @@ -231,6 +223,7 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelR map.name = "temperature" map.unit = getTemperatureScale() map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C")) + updateThermostatSetpoint(null, null) } else if (cmd.sensorType == 5) { map.value = cmd.scaledSensorValue map.unit = "%" @@ -240,7 +233,7 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelR } def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) { - def map = [name: "thermostatOperatingState" ] + def map = [name: "thermostatOperatingState"] switch (cmd.operatingState) { case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: map.value = "idle" @@ -302,15 +295,30 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeRepor map.value = "auto" break } - state.lastTriedMode = map.value sendEvent(map) - updateThermostatSetpoint(null, null) + // Now that mode and temperature is known we can request setpoints in correct order + // Also makes sure operating state is in sync as it isn't being reported when changed + def cmds = [] + def heatingSetpoint = getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = getTempInLocalScale("coolingSetpoint") + def currentTemperature = getTempInLocalScale("temperature") + cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) + if (map.value == "cool" || ((map.value == "auto" || map.value == "off") && (currentTemperature > (heatingSetpoint + coolingSetpoint)/2))) { + // request cooling setpoint first + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) // CoolingSetpoint + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) // HeatingSetpoint + } else { + // request heating setpoint first + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) // HeatingSetpoint + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) // CoolingSetpoint + } + sendHubCommand(cmds) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { def map = [name: "thermostatFanMode", data:[supportedThermostatFanModes: state.supportedFanModes]] switch (cmd.fanMode) { - case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: map.value = "auto" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: @@ -320,7 +328,6 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanMod map.value = "circulate" break } - state.lastTriedFanMode = map.value sendEvent(map) } @@ -398,64 +405,65 @@ def poll() { def cmds = [] cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 1).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // temperature - cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) // HeatingSetpoint - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) // CoolingSetpoint cmds << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 2).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // humidity + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 1).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // temperature + cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) def time = getTimeAndDay() -log.debug "time: $time" if (time) { cmds << new physicalgraph.device.HubAction(zwave.clockV1.clockSet(time).format()) } - // Add 3 seconds delay between each command to avoid flooding the Z-Wave network choking the hub - sendHubCommand(cmds, 3000) + // ThermostatModeReport will spawn request for operating state and setpoints so request this last + // this as temperature and mode is needed to determine which setpoints should be requested first + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) + // Add 2 seconds delay between each command to avoid flooding the Z-Wave network choking the hub + sendHubCommand(cmds, 2000) } def raiseHeatingSetpoint() { - alterSetpoint(null, true, "heatingSetpoint") + alterSetpoint(true, "heatingSetpoint") } def lowerHeatingSetpoint() { - alterSetpoint(null, false, "heatingSetpoint") + alterSetpoint(false, "heatingSetpoint") } def raiseCoolSetpoint() { - alterSetpoint(null, true, "coolingSetpoint") + alterSetpoint(true, "coolingSetpoint") } def lowerCoolSetpoint() { - alterSetpoint(null, false, "coolingSetpoint") + alterSetpoint(false, "coolingSetpoint") } // Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false -def alterSetpoint(degrees, raise, setpoint) { +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 targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint def delta = (locationScale == "F") ? 1 : 0.5 - if (raise != null) { - targetvalue += raise ? delta : - delta - } else if (degrees) { - targetvalue = degrees - } else { - log.warn "alterSetpoint called with neither up/down/degree information" - return - } - def data = enforceSetpointLimits(setpoint, [targetvalue: targetvalue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) + targetValue += raise ? delta : - delta + + def data = enforceSetpointLimits(setpoint, [targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint], raise) // 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": data.targetHeatingSetpoint, unit: locationScale, eventType: "ENTITY_UPDATE")//, displayed: false) - runIn(4, "updateHeatingSetpoint", [data: data, overwrite: true]) + sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, deviceScale), + unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) } if (data.targetCoolingSetpoint) { - sendEvent("name": "coolingSetpoint", "value": data.targetCoolingSetpoint, unit: locationScale, eventType: "ENTITY_UPDATE")//, displayed: false) - runIn(4, "updateCoolingSetpoint", [data: data, overwrite: true]) + 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]) } } @@ -467,69 +475,138 @@ def updateCoolingSetpoint(data) { updateSetpoints(data) } -def enforceSetpointLimits(setpoint, data) { - // Enforce max/min for setpoints - def maxSetpoint = getTempInLocalScale(95, "F") - def minSetpoint = getTempInLocalScale(35, "F") - def targetvalue = data.targetvalue +def updateEnforceSetpointLimits(setpoint, setpointValue) { + def heatingSetpoint = (setpoint == "heatingSetpoint") ? setpointValue : getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = (setpoint == "coolingSetpoint") ? setpointValue : getTempInLocalScale("coolingSetpoint") + + sendEvent(name: setpoint, value: setpointValue, unit: getTemperatureScale(), displayed: false) + updateThermostatSetpoint(setpoint, setpointValue) + // Enforce coolingSetpoint limits, as device doesn't + def data = enforceSetpointLimits(setpoint, [targetValue: setpointValue, + heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) + if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { + data.targetHeatingSetpoint = null + } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { + data.targetCoolingSetpoint = null + } + if (data.targetHeatingSetpoint != null || data.targetCoolingSetpoint != null) { + updateSetpoints(data) + } +} + +def enforceSetpointLimits(setpoint, data, raise = null) { + def locationScale = getTemperatureScale() + def deviceScale = (state.scale == 1) ? "F" : "C" + // min/max with 3°F/2°C deadband consideration + def minSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(35, "F") : getTempInDeviceScale(38, "F") + def maxSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(92, "F") : getTempInDeviceScale(95, "F") + def deadband = (deviceScale == "F") ? 3 : 2 + def delta = (locationScale == "F") ? 1 : 0.5 + def targetValue = getTempInDeviceScale(data.targetValue, locationScale) def heatingSetpoint = null def coolingSetpoint = null - if (targetvalue > maxSetpoint) { - targetvalue = maxSetpoint - } else if (targetvalue < minSetpoint) { - targetvalue = minSetpoint + // Enforce min/mix for setpoints + if (targetValue > maxSetpoint) { + heatingSetpoint = (setpoint == "heatingSetpoint") ? maxSetpoint : getTempInDeviceScale(data.heatingSetpoint, locationScale) + coolingSetpoint = (setpoint == "heatingSetpoint") ? maxSetpoint + deadband : maxSetpoint + } else if (targetValue < minSetpoint) { + heatingSetpoint = (setpoint == "coolingSetpoint") ? minSetpoint - deadband : minSetpoint + coolingSetpoint = (setpoint == "coolingSetpoint") ? minSetpoint : getTempInDeviceScale(data.coolingSetpoint, locationScale) } - // Enforce limits, for now make sure heating <= cooling, and cooling >= heating - if (setpoint == "heatingSetpoint") { - heatingSetpoint = targetvalue - coolingSetpoint = (heatingSetpoint > data.coolingSetpoint) ? heatingSetpoint : null + // Enforce deadband between setpoints + if (setpoint == "heatingSetpoint" && !coolingSetpoint) { + // Note if new value is same as old value we need to move it in the direction the user wants to change it, 1°F or 0.5°C, + heatingSetpoint = (targetValue != getTempInDeviceScale(data.heatingSetpoint, locationScale) || !raise) ? + targetValue : (raise ? targetValue + delta : targetValue - delta) + coolingSetpoint = (heatingSetpoint + deadband > getTempInDeviceScale(data.coolingSetpoint, locationScale)) ? heatingSetpoint + deadband : null } - if (setpoint == "coolingSetpoint") { - coolingSetpoint = targetvalue - heatingSetpoint = (coolingSetpoint < data.heatingSetpoint) ? coolingSetpoint : null + if (setpoint == "coolingSetpoint" && !heatingSetpoint) { + coolingSetpoint = (targetValue != getTempInDeviceScale(data.coolingSetpoint, locationScale) || !raise) ? + targetValue : (raise ? targetValue + delta : targetValue - delta) + heatingSetpoint = (coolingSetpoint - deadband < getTempInDeviceScale(data.heatingSetpoint, locationScale)) ? coolingSetpoint - deadband : null } return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint] } def setHeatingSetpoint(degrees) { if (degrees) { - def data = enforceSetpointLimits("heatingSetpoint", - [targetvalue: degrees.toDouble(), heatingSetpoint: getTempInLocalScale("heatingSetpoint"), coolingSetpoint: getTempInLocalScale("coolingSetpoint")]) - updateSetpoints(data) + state.heatingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) } } def setCoolingSetpoint(degrees) { if (degrees) { - def data = enforceSetpointLimits("coolingSetpoint", - [targetvalue: degrees.toDouble(), heatingSetpoint: getTempInLocalScale("heatingSetpoint"), coolingSetpoint: getTempInLocalScale("coolingSetpoint")]) - updateSetpoints(data) + 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) { + unschedule("updateSetpoints") def cmds = [] if (data.targetHeatingSetpoint) { + state.targetHeatingSetpoint = data.targetHeatingSetpoint cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( - setpointType: 1, scale: state.scale, precision: state.precision, scaledValue: convertToDeviceScale(data.targetHeatingSetpoint)).format()) + setpointType: 1, scale: state.scale, precision: state.precision, scaledValue: data.targetHeatingSetpoint).format()) } if (data.targetCoolingSetpoint) { + state.targetCoolingSetpoint = data.targetCoolingSetpoint cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( - setpointType: 2, scale: state.scale, precision: state.precision, scaledValue: convertToDeviceScale(data.targetCoolingSetpoint)).format()) + setpointType: 2, scale: state.scale, precision: state.precision, scaledValue: data.targetCoolingSetpoint).format()) + } + // Also make sure temperature and operating state is in sync + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 1).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // temperature + cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) + if (data.targetHeatingSetpoint) { + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) + } + if (data.targetCoolingSetpoint) { + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) } - - // Always request both setpoints in case thermostat changed both - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) sendHubCommand(cmds) } -def convertToDeviceScale(setpoint) { - def locationScale = getTemperatureScale() - def deviceScale = (state.scale == 1) ? "F" : "C" - return (deviceScale == locationScale) ? setpoint : - (deviceScale == "F" ? celsiusToFahrenheit(setpoint.toBigDecimal()) : roundC(fahrenheitToCelsius(setpoint.toBigDecimal()))) +// 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()) } /** @@ -543,54 +620,68 @@ def ping() { def switchMode() { def currentMode = device.currentValue("thermostatMode") - def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" def supportedModes = state.supportedModes if (supportedModes) { def next = { supportedModes[supportedModes.indexOf(it) + 1] ?: supportedModes[0] } - def nextMode = next(lastTriedMode) - setThermostatMode(nextMode) - state.lastTriedMode = nextMode + def nextMode = next(currentMode) + runIn(2, "setThermostatMode", [data: [nextMode: nextMode], overwrite: true]) } else { log.warn "supportedModes not defined" + getSupportedModes() } } def switchToMode(nextMode) { def supportedModes = state.supportedModes - if (supportedModes && supportedModes.contains(nextMode)) { - setThermostatMode(nextMode) - state.lastTriedMode = nextMode + 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("ThermostatMode $nextMode is not supported by ${device.displayName}") + 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 currentMode = device.currentValue("thermostatFanMode") def supportedFanModes = state.supportedFanModes if (supportedFanModes) { def next = { supportedFanModes[supportedFanModes.indexOf(it) + 1] ?: supportedFanModes[0] } - def nextMode = next(lastTriedMode) - setThermostatFanMode(nextMode) - state.lastTriedFanMode = nextMode + def nextMode = next(currentMode) + runIn(2, "setThermostatFanMode", [data: [nextMode: nextMode], overwrite: true]) } else { log.warn "supportedFanModes not defined" + getSupportedFanModes() } } def switchToFanMode(nextMode) { def supportedFanModes = state.supportedFanModes - if (supportedFanModes && supportedFanModes.contains(nextMode)) { - setThermostatFanMode(nextMode) - state.lastTriedFanMode = nextMode + 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("FanMode $nextMode is not supported by ${device.displayName}") + log.warn "supportedFanModes not defined" + getSupportedFanModes() } } -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() { [ @@ -602,8 +693,12 @@ def getModeMap() { [ ]} def setThermostatMode(String value) { + switchToMode(value) +} + +def setThermostatMode(data) { def cmds = [] - cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]).format()) cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) sendHubCommand(cmds) } @@ -615,8 +710,12 @@ def getFanModeMap() { [ ]} def setThermostatFanMode(String value) { + switchToFanMode(value) +} + +def setThermostatFanMode(data) { def cmds = [] - cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]).format()) cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format()) sendHubCommand(cmds) } @@ -675,8 +774,28 @@ def getTempInLocalScale(state) { // get/convert temperature to current local scale def getTempInLocalScale(temp, scale) { - def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble() - return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp)) + 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).toDouble().round(0).toInteger() : roundC(fahrenheitToCelsius(temp))) + } + return 0 } def roundC (tempC) { From 3a101398dd5fc3f667f6e1a8179f421948e3e386 Mon Sep 17 00:00:00 2001 From: Ingvar Marstorp Date: Thu, 3 Aug 2017 15:20:55 -0700 Subject: [PATCH 8/8] Update zwave-thermostat.groovy --- .../zwave-thermostat.groovy | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index d8e7cd7..e7205e1 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -264,13 +264,13 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanMod 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 } sendEvent(map) @@ -381,9 +381,9 @@ def alterSetpoint(raise, setpoint) { } if (data.targetHeatingSetpoint && data.targetCoolingSetpoint) { runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) - } else if (setpoint == "heatingSetpoint") { + } else if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) - } else if (setpoint == "coolingSetpoint") { + } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true]) } } @@ -424,22 +424,39 @@ def enforceSetpointLimits(setpoint, data) { def setHeatingSetpoint(degrees) { if (degrees) { - def data = enforceSetpointLimits("heatingSetpoint", - [targetValue: getTempInDeviceScale(degrees.toDouble(), getTemperatureScale()), - heatingSetpoint: getTempInDeviceScale("heatingSetpoint"), coolingSetpoint: getTempInDeviceScale("coolingSetpoint")]) - updateSetpoints(data) + state.heatingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) } } def setCoolingSetpoint(degrees) { if (degrees) { - def data = enforceSetpointLimits("coolingSetpoint", - [targetValue: getTempInDeviceScale(degrees.toDouble(), getTemperatureScale()), - heatingSetpoint: getTempInDeviceScale("heatingSetpoint"), coolingSetpoint: getTempInDeviceScale("coolingSetpoint")]) - updateSetpoints(data) + 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) {