diff --git a/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy index f2e1154..f8bd783 100644 --- a/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy +++ b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy @@ -73,7 +73,7 @@ metadata { [value: 64, color: "#44B621"], [value: 80, color: "#3D79D9"], [value: 96, color: "#0A50C2"] - ] + ], icon:"st.Weather.weather12" } valueTile("maxHum", "device.maxHum", canChangeIcon: false, canChangeBackground: false) { diff --git a/devicetypes/smartthings/aeon-multisensor-gen5.src/README.md b/devicetypes/smartthings/aeon-multisensor-gen5.src/README.md index 62a8f9a..800febb 100644 --- a/devicetypes/smartthings/aeon-multisensor-gen5.src/README.md +++ b/devicetypes/smartthings/aeon-multisensor-gen5.src/README.md @@ -27,13 +27,9 @@ Works with: ## Device Health Aeon Labs MultiSensor (Gen 5) is polled by the hub. -As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. -Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. -Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for -the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, -it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. +Aeon MultiSensor Gen5 reports in once every hour. -* __32min__ checkInterval +* __122min__ checkInterval ## Troubleshooting diff --git a/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy b/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy index 5b31e3d..6f48b0c 100644 --- a/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy +++ b/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy @@ -100,12 +100,12 @@ metadata { def installed(){ // 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]) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } def updated(){ // 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]) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } def parse(String description) diff --git a/devicetypes/smartthings/aeon-multisensor.src/README.md b/devicetypes/smartthings/aeon-multisensor.src/README.md index aa61b0d..16e60d5 100644 --- a/devicetypes/smartthings/aeon-multisensor.src/README.md +++ b/devicetypes/smartthings/aeon-multisensor.src/README.md @@ -28,13 +28,9 @@ Works with: ## Device Health Aeon Labs MultiSensor is polled by the hub. -As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. -Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. -Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for -the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, -it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. +Aeon MultiSensor reports in once every hour. -* __32min__ checkInterval +* __122min__ checkInterval ## Battery Specification diff --git a/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy index b6f3135..27c85e3 100644 --- a/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy +++ b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy @@ -96,12 +96,12 @@ metadata { def installed(){ // 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]) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } def updated(){ // 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]) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } // Parse incoming device messages to generate events diff --git a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy index ea1e7af..ad313d2 100644 --- a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy +++ b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy @@ -6,7 +6,6 @@ metadata { capability "Relative Humidity Measurement" capability "Thermostat" capability "Battery" - capability "Configuration" capability "Refresh" capability "Sensor" capability "Health Check" @@ -15,161 +14,173 @@ metadata { command "switchMode" command "switchFanMode" - command "quickSetCool" - command "quickSetHeat" + command "lowerHeatingSetpoint" + command "raiseHeatingSetpoint" + command "lowerCoolSetpoint" + command "raiseCoolSetpoint" fingerprint deviceId: "0x08", inClusters: "0x43,0x40,0x44,0x31,0x80,0x85,0x60" fingerprint mfr:"0098", prod:"6401", model:"0107", deviceJoinName: "2Gig CT100 Programmable 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 "fanAuto" : "command: 4403, payload: 00" - status "fanOn" : "command: 4403, payload: 01" - status "fanCirculate" : "command: 4403, payload: 06" - - status "heat 60" : "command: 4303, payload: 01 09 3C" - status "heat 72" : "command: 4303, payload: 01 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 78" : "command: 3105, payload: 01 2A 03 0C" - status "temp 86" : "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" - - // reply messages - reply "2502": "command: 2503, payload: FF" - } - tiles { - valueTile("temperature", "device.temperature", width: 2, height: 2) { - state("temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 32, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 92, color: "#d04e00"], - [value: 98, 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"] + ] + ) + } + tileAttribute("device.batteryIcon", key: "SECONDARY_CONTROL") { + attributeState "ok_battery", label:'${currentValue}%', icon:"st.arlo.sensor_battery_4" + attributeState "low_battery", label:'Low Battery', icon:"st.arlo.sensor_battery_0" + } } - standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { - state "off", label:'${name}', action:"switchMode", nextState:"to_heat" - state "heat", label:'${name}', action:"switchMode", nextState:"to_cool" - state "cool", label:'${name}', action:"switchMode", nextState:"..." - state "auto", label:'${name}', action:"switchMode", nextState:"..." - state "emergency heat", label:'${name}', action:"switchMode", nextState:"..." - state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool" - state "to_cool", label: "cool", action:"switchMode", nextState:"..." - state "...", label: "...", action:"off", nextState:"off" + standardTile("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 "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" } - standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { - state "fanAuto", label:'${name}', action:"switchFanMode" - state "fanOn", label:'${name}', action:"switchFanMode" - state "fanCirculate", label:'${name}', action:"switchFanMode" + 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" } - controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#e86d13" + valueTile("humidity", "device.humidity", width:2, height:2, inactiveLabel: false, decoration: "flat") { + state "humidity", label:'${currentValue}%', icon:"st.Weather.weather12" } - valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { - state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff" + standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left" } - controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#00a0dc" + valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff" } - valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff" + standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right" } - valueTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat") { - state "humidity", label:'${currentValue}% humidity', unit:"" + standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left" } - valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" + valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff" } - standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right" + } + + 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", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "humidity", "battery"]) + details(["temperature", "mode", "fanMode", "humidity", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint", "coolingSetpoint", "raiseCoolSetpoint", "refresh"]) } } def updated() { - // 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]) + // 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() +} + +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]) + // Poll device for additional data that will be updated by refresh tile + refresh() } def parse(String description) { - def result = [] + def result = null if (description == "updated") { } else { def zwcmd = zwave.parse(description, [0x42:2, 0x43:2, 0x31: 2, 0x60: 3]) if (zwcmd) { - result += zwaveEvent(zwcmd) + result = zwaveEvent(zwcmd) + // Check battery level at least once every 2 days + if (!state.lastbatt || now() - state.lastbatt > 48*60*60*1000) { + sendHubCommand(new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())) + } } else { log.debug "$device.displayName couldn't parse $description" } } if (!result) { - return null + return [] } - if (result.size() == 1 && (!state.lastbatt || now() - state.lastbatt > 48*60*60*1000)) { - result << response(zwave.batteryV1.batteryGet().format()) - } - log.debug "$device.displayName parsed '$description' to $result" - result + return [result] } -def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { - def result = null - def encapsulatedCommand = cmd.encapsulatedCommand([0x42:2, 0x43:2, 0x31: 2]) - log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiInstanceCmdEncap cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 3]) + log.debug ("multiinstancev1.MultiInstanceCmdEncap: command from instance ${cmd.instance}: ${encapsulatedCommand}") if (encapsulatedCommand) { - result = zwaveEvent(encapsulatedCommand) - if (cmd.sourceEndPoint == 1) { // indicates a response to refresh() vs an unrequested update - def event = ([] + result)[0] // in case zwaveEvent returns a list - def resp = nextRefreshQuery(event?.name) - if (resp) { - log.debug("sending next refresh query: $resp") - result = [] + result + response(["delay 200", resp]) - } - } + zwaveEvent(encapsulatedCommand) } - result } def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { - def cmdScale = cmd.scale == 1 ? "F" : "C" - def temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) + def sendCmd = [] def unit = getTemperatureScale() - def map1 = [ value: temp, unit: unit, displayed: false ] + def cmdScale = cmd.scale == 1 ? "F" : "C" + def setpoint = getTempInLocalScale(cmd.scaledValue, cmdScale) switch (cmd.setpointType) { case 1: - map1.name = "heatingSetpoint" + //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) + } break; case 2: - map1.name = "coolingSetpoint" + //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) + } break; default: log.debug "unknown setpointType $cmd.setpointType" @@ -180,33 +191,55 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpo state.size = cmd.size state.scale = cmd.scale state.precision = cmd.precision - - def mode = device.latestValue("thermostatMode") - if (mode && map1.name.startsWith(mode) || (mode == "emergency heat" && map1.name == "heatingSetpoint")) { - def map2 = [ name: "thermostatSetpoint", value: temp, unit: unit ] - [ createEvent(map1), createEvent(map2) ] - } else { - createEvent(map1) - } } -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) -{ +// 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) { def map = [:] if (cmd.sensorType == 1) { map.name = "temperature" map.unit = getTemperatureScale() - map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C")) } else if (cmd.sensorType == 5) { map.name = "humidity" map.unit = "%" map.value = cmd.scaledSensorValue } - createEvent(map) + sendEvent(map) } -def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) { + def map = [:] + if (cmd.sensorType == 1) { + map.name = "temperature" + map.unit = getTemperatureScale() + map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C")) + } else if (cmd.sensorType == 5) { + map.value = cmd.scaledSensorValue + map.unit = "%" + map.name = "humidity" + } + sendEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) { def map = [name: "thermostatOperatingState" ] switch (cmd.operatingState) { case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: @@ -231,12 +264,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.Thermosta map.value = "vent economizer" break } - def result = createEvent(map) - if (result.isStateChange && device.latestValue("thermostatMode") == "auto" && (result.value == "heating" || result.value == "cooling")) { - def thermostatSetpoint = device.latestValue("${result.value}Setpoint") - result = [result, createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale())] - } - result + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { @@ -252,203 +280,256 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanSt map.value = "running high" break } - createEvent(map) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { - def map = [name: "thermostatMode"] - def thermostatSetpoint = null + def map = [name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes]] switch (cmd.mode) { case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: map.value = "off" break case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT: map.value = "heat" - thermostatSetpoint = device.latestValue("heatingSetpoint") break case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT: map.value = "emergency heat" - thermostatSetpoint = device.latestValue("heatingSetpoint") break case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: map.value = "cool" - thermostatSetpoint = device.latestValue("coolingSetpoint") break case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO: map.value = "auto" - def temp = device.latestValue("temperature") - def heatingSetpoint = device.latestValue("heatingSetpoint") - def coolingSetpoint = device.latestValue("coolingSetpoint") - if (temp && heatingSetpoint && coolingSetpoint) { - if (temp < (heatingSetpoint + coolingSetpoint) / 2.0) { - thermostatSetpoint = heatingSetpoint - } else { - thermostatSetpoint = coolingSetpoint - } - } break } state.lastTriedMode = map.value - if (thermostatSetpoint) { - [ createEvent(map), createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) ] - } else { - createEvent(map) - } + sendEvent(map) + updateThermostatSetpoint(null, null) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { - def map = [name: "thermostatFanMode", displayed: false] + def map = [name: "thermostatFanMode", data:[supportedThermostatFanModes: state.supportedFanModes]] switch (cmd.fanMode) { - case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: - map.value = "fanAuto" + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: + map.value = "auto" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: - map.value = "fanOn" + map.value = "on" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: - map.value = "fanCirculate" + map.value = "circulate" break } state.lastTriedFanMode = map.value - createEvent(map) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { - def supportedModes = "" - if(cmd.off) { supportedModes += "off " } - if(cmd.heat) { supportedModes += "heat " } - if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " } - if(cmd.cool) { supportedModes += "cool " } - if(cmd.auto) { supportedModes += "auto " } + def supportedModes = [] + if(cmd.heat) { supportedModes << "heat" } + if(cmd.cool) { supportedModes << "cool" } + // Make sure off is before auto, this ensures the right setpoint is used based on current temperature when auto is set + if(cmd.off) { supportedModes << "off" } + if(cmd.auto) { supportedModes << "auto" } + if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" } state.supportedModes = supportedModes - [ createEvent(name:"supportedModes", value: supportedModes, displayed: false), - response(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet()) ] + sendEvent(name: "supportedThermostatModes", value: supportedModes, isStateChange: true, displayed: false) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { - def supportedFanModes = "" - if(cmd.auto) { supportedFanModes += "fanAuto " } - if(cmd.low) { supportedFanModes += "fanOn " } - if(cmd.circulation) { supportedFanModes += "fanCirculate " } + def supportedFanModes = [] + if(cmd.auto) { supportedFanModes << "auto" } + if(cmd.low) { supportedFanModes << "on" } + if(cmd.circulation) { supportedFanModes << "circulate" } state.supportedFanModes = supportedFanModes - [ createEvent(name:"supportedFanModes", value: supportedModes, displayed: false), - response(refresh()) ] + sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, isStateChange: true, displayed: false) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - log.debug "Zwave event received: $cmd" + log.debug "Zwave BasicReport: $cmd" } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] - if (cmd.batteryLevel == 0xFF) { + def batteryState = cmd.batteryLevel + def map = [name: "battery", unit: "%", value: cmd.batteryLevel] + if ((cmd.batteryLevel == 0xFF) || (cmd.batteryLevel == 0x00)) { // Special value for low battery alert map.value = 1 map.descriptionText = "${device.displayName} battery is low" map.isStateChange = true - } else { - map.value = cmd.batteryLevel + batteryState = "low_battery" } state.lastbatt = now() - createEvent(map) + sendEvent(name: "batteryIcon", value: batteryState, displayed: false) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.Command cmd) { log.warn "Unexpected zwave command $cmd" } +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { +log.debug "ManufacturerSpecificReport ${cmd}: value:${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 refresh() { - // Use encapsulation to differentiate refresh cmds from what the thermostat sends proactively on change - def cmd = zwave.sensorMultilevelV2.sensorMultilevelGet() - zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format() -} - -def nextRefreshQuery(name) { - def cmd = null - switch (name) { - case "temperature": - cmd = zwave.thermostatModeV2.thermostatModeGet() - break - case "thermostatMode": - cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) - break - case "heatingSetpoint": - cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) - break - case "coolingSetpoint": - cmd = zwave.thermostatFanModeV3.thermostatFanModeGet() - break - case "thermostatFanMode": - cmd = zwave.thermostatOperatingStateV2.thermostatOperatingStateGet() - break - case "thermostatOperatingState": - // get humidity, multilevel sensor get to endpoint 2 - cmd = zwave.sensorMultilevelV2.sensorMultilevelGet() - return zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:2).encapsulate(cmd).format() - default: return null + // 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 + // refresh will request battery, prevent multiple request by setting lastbatt now + state.lastbatt = timeNow + // use runIn with overwrite to prevent multiple DTH instances run before state.refreshTriggeredAt has been saved + runIn(2, "poll", [overwrite: true]) } - zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format() } -def quickSetHeat(degrees) { - setHeatingSetpoint(degrees, 1000) +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 + 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) } -def setHeatingSetpoint(degrees, delay = 30000) { - setHeatingSetpoint(degrees.toDouble(), delay) +def raiseHeatingSetpoint() { + alterSetpoint(null, true, "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 lowerHeatingSetpoint() { + alterSetpoint(null, false, "heatingSetpoint") +} + +def raiseCoolSetpoint() { + alterSetpoint(null, true, "coolingSetpoint") +} + +def lowerCoolSetpoint() { + alterSetpoint(null, false, "coolingSetpoint") +} + +// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false +def alterSetpoint(degrees, raise, setpoint) { 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) + def heatingSetpoint = getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = getTempInLocalScale("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 { - convertedDegrees = degrees + log.warn "alterSetpoint called with neither up/down/degree information" + return + } + 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": data.targetHeatingSetpoint, unit: locationScale, eventType: "ENTITY_UPDATE")//, displayed: false) + runIn(4, "updateHeatingSetpoint", [data: data, overwrite: true]) + } + if (data.targetCoolingSetpoint) { + sendEvent("name": "coolingSetpoint", "value": data.targetCoolingSetpoint, unit: locationScale, eventType: "ENTITY_UPDATE")//, displayed: false) + runIn(4, "updateCoolingSetpoint", [data: data, overwrite: true]) + } +} + +def updateHeatingSetpoint(data) { + updateSetpoints(data) +} + +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 heatingSetpoint = null + def coolingSetpoint = null + + if (targetvalue > maxSetpoint) { + targetvalue = maxSetpoint + } else if (targetvalue < minSetpoint) { + targetvalue = minSetpoint + } + // Enforce limits, for now make sure heating <= cooling, and cooling >= heating + if (setpoint == "heatingSetpoint") { + heatingSetpoint = targetvalue + coolingSetpoint = (heatingSetpoint > data.coolingSetpoint) ? heatingSetpoint : null + } + if (setpoint == "coolingSetpoint") { + coolingSetpoint = targetvalue + heatingSetpoint = (coolingSetpoint < data.heatingSetpoint) ? coolingSetpoint : 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) + } +} + +def setCoolingSetpoint(degrees) { + if (degrees) { + def data = enforceSetpointLimits("coolingSetpoint", + [targetvalue: degrees.toDouble(), heatingSetpoint: getTempInLocalScale("heatingSetpoint"), coolingSetpoint: getTempInLocalScale("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: convertToDeviceScale(data.targetHeatingSetpoint)).format()) + } + if (data.targetCoolingSetpoint) { + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( + setpointType: 2, scale: state.scale, precision: state.precision, scaledValue: convertToDeviceScale(data.targetCoolingSetpoint)).format()) } - delayBetween([ - zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format() - ], delay) + // 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 quickSetCool(degrees) { - setCoolingSetpoint(degrees, 1000) -} - -def setCoolingSetpoint(degrees, delay = 30000) { - setCoolingSetpoint(degrees.toDouble(), delay) -} - -def setCoolingSetpoint(Double degrees, Integer delay = 30000) { - log.trace "setCoolingSetpoint($degrees, $delay)" - def deviceScale = state.scale ?: 1 - def deviceScaleString = deviceScale == 2 ? "C" : "F" +def convertToDeviceScale(setpoint) { def locationScale = getTemperatureScale() - def p = (state.precision == null) ? 1 : state.precision - - def convertedDegrees - if (locationScale == "C" && deviceScaleString == "F") { - convertedDegrees = celsiusToFahrenheit(degrees) - } else if (locationScale == "F" && deviceScaleString == "C") { - convertedDegrees = fahrenheitToCelsius(degrees) - } else { - convertedDegrees = degrees - } - - delayBetween([ - zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format() - ], delay) + def deviceScale = (state.scale == 1) ? "F" : "C" + return (deviceScale == locationScale) ? setpoint : + (deviceScale == "F" ? celsiusToFahrenheit(setpoint.toBigDecimal()) : roundC(fahrenheitToCelsius(setpoint.toBigDecimal()))) } /** @@ -456,78 +537,56 @@ def setCoolingSetpoint(Double degrees, Integer delay = 30000) { * */ def ping() { log.debug "ping() called" - refresh() -} - -def configure() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSupportedGet().format(), - ], 2300) -} - -def modes() { - ["off", "heat", "cool", "auto", "emergency heat"] + // Just get Operating State as it is not reported when it chnages and 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 currentMode = device.currentValue("thermostatMode") def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" - def supportedModes = getDataByName("supportedModes") - def modeOrder = modes() - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - if (supportedModes?.contains(currentMode)) { - while (!supportedModes.contains(nextMode) && nextMode != "off") { - nextMode = next(nextMode) - } + def supportedModes = state.supportedModes + if (supportedModes) { + def next = { supportedModes[supportedModes.indexOf(it) + 1] ?: supportedModes[0] } + def nextMode = next(lastTriedMode) + setThermostatMode(nextMode) + state.lastTriedMode = nextMode + } else { + log.warn "supportedModes not defined" } - state.lastTriedMode = nextMode - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], 1000) } def switchToMode(nextMode) { - def supportedModes = getDataByName("supportedModes") - if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - if (nextMode in modes()) { + def supportedModes = state.supportedModes + if (supportedModes && supportedModes.contains(nextMode)) { + setThermostatMode(nextMode) state.lastTriedMode = nextMode - "$nextMode"() } else { - log.debug("no mode method '$nextMode'") + log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}") } } def switchFanMode() { def currentMode = device.currentState("thermostatFanMode")?.value def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off" - def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn" - def modeOrder = ["fanAuto", "fanCirculate", "fanOn"] - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") { - nextMode = next(nextMode) + def supportedFanModes = state.supportedFanModes + if (supportedFanModes) { + def next = { supportedFanModes[supportedFanModes.indexOf(it) + 1] ?: supportedFanModes[0] } + def nextMode = next(lastTriedMode) + setThermostatFanMode(nextMode) + state.lastTriedFanMode = nextMode + } else { + log.warn "supportedFanModes not defined" } - switchToFanMode(nextMode) } def switchToFanMode(nextMode) { - def supportedFanModes = getDataByName("supportedFanModes") - if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - - def returnCommand - if (nextMode == "fanAuto") { - returnCommand = fanAuto() - } else if (nextMode == "fanOn") { - returnCommand = fanOn() - } else if (nextMode == "fanCirculate") { - returnCommand = fanCirculate() + def supportedFanModes = state.supportedFanModes + if (supportedFanModes && supportedFanModes.contains(nextMode)) { + setThermostatFanMode(nextMode) + state.lastTriedFanMode = nextMode } else { - log.debug("no fan mode '$nextMode'") + log.debug("FanMode $nextMode is not supported by ${device.displayName}") } - if(returnCommand) state.lastTriedFanMode = nextMode - returnCommand } def getDataByName(String name) { @@ -543,10 +602,10 @@ def getModeMap() { [ ]} def setThermostatMode(String value) { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) + sendHubCommand(cmds) } def getFanModeMap() { [ @@ -556,69 +615,70 @@ def getFanModeMap() { [ ]} def setThermostatFanMode(String value) { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).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 +private getTimeAndDay() { + def timeNow = now() + // Need to check that location have timeZone as SC may have created the location without setting it + // Don't update clock more than once a day + if (location.timeZone && (!state.timeClockSet || (24 * 60 * 60 * 1000 < (timeNow - state.timeClockSet)))) { + def currentDate = Calendar.getInstance(location.timeZone) + state.timeClockSet = timeNow + return [hour: currentDate.get(Calendar.HOUR_OF_DAY), minute: currentDate.get(Calendar.MINUTE), weekday: currentDate.get(Calendar.DAY_OF_WEEK)] + } } +// 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) { + def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble() + return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp)) +} + +def roundC (tempC) { + return (Math.round(tempC.toDouble() * 2))/2 +} diff --git a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/.st-ignore b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md new file mode 100644 index 0000000..8baf48f --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md @@ -0,0 +1,39 @@ +# Z-wave Basic Smoke Alarm + +Cloud Execution + +Works with: + +* [First Alert Smoke Detector (ZSMOKE)](https://www.smartthings.com/products/first-alert-smoke-detector) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Smoke Detector** - measure smoke and optionally carbon monoxide levels +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +First Alert Smoke Detector (ZSMOKE) is a Z-wave sleepy device and checks in every 1 hour. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*60 + 2)mins = 122 mins. + +* __122min__ checkInterval + +## Battery Specification + +Two AA 1.5V batteries are required. + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. +Pairing needs to be tried again by placing the device closer to the hub. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [First Alert Smoke Detector (ZSMOKE) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207150556-First-Alert-Smoke-Detector-ZSMOKE-) diff --git a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy new file mode 100644 index 0000000..a2d7df2 --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy @@ -0,0 +1,181 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Basic Smoke Alarm", namespace: "smartthings", author: "SmartThings") { + capability "Smoke Detector" + capability "Sensor" + capability "Battery" + capability "Health Check" + + fingerprint deviceId: "0xA100", inClusters: "0x20,0x80,0x70,0x85,0x71,0x72,0x86" + fingerprint mfr:"0138", prod:"0001", model:"0001", deviceJoinName: "First Alert Smoke Detector" + } + + simulator { + status "smoke": "command: 7105, payload: 01 FF" + status "clear": "command: 7105, payload: 01 00" + status "test": "command: 7105, payload: 0C FF" + status "battery 100%": "command: 8003, payload: 64" + status "battery 5%": "command: 8003, payload: 05" + } + + tiles (scale: 2){ + multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){ + tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "smoke" + details(["smoke", "battery"]) + } +} + +def installed() { +// Device checks in every hour, this interval allows us to miss one check-in notification before marking offline + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + def cmds = [] + createSmokeEvents("smokeClear", cmds) + cmds.each { cmd -> sendEvent(cmd) } +} + +def updated() { +// Device checks in every hour, this interval allows us to miss one check-in notification before marking offline + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results << createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + if (cmd) { + zwaveEvent(cmd, results) + } + } + log.debug "'$description' parsed to ${results.inspect()}" + return results +} + +def createSmokeEvents(name, results) { + def text = null + switch (name) { + case "smoke": + text = "$device.displayName smoke was detected!" + // these are displayed:false because the composite event is the one we want to see in the app + results << createEvent(name: "smoke", value: "detected", descriptionText: text) + break + case "tested": + text = "$device.displayName was tested" + results << createEvent(name: "smoke", value: "tested", descriptionText: text) + break + case "smokeClear": + text = "$device.displayName smoke is clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text) + name = "clear" + break + case "testClear": + text = "$device.displayName test cleared" + results << createEvent(name: "smoke", value: "clear", descriptionText: text) + name = "clear" + break + } +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { + if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_SMOKE) { + if (cmd.zwaveAlarmEvent == 3) { + createSmokeEvents("tested", results) + } else { + createSmokeEvents((cmd.zwaveAlarmEvent == 1 || cmd.zwaveAlarmEvent == 2) ? "smoke" : "smokeClear", results) + } + } else switch(cmd.alarmType) { + case 1: + createSmokeEvents(cmd.alarmLevel ? "smoke" : "smokeClear", results) + break + case 12: // test button pressed + createSmokeEvents(cmd.alarmLevel ? "tested" : "testClear", results) + break + case 13: // sent every hour -- not sure what this means, just a wake up notification? + if (cmd.alarmLevel == 255) { + results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false) + } else { + results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", isStateChange:true, displayed:false) + } + + // Clear smoke in case they pulled batteries and we missed the clear msg + if(device.currentValue("smoke") != "clear") { + createSmokeEvents("smokeClear", results) + } + + // Check battery if we don't have a recent battery event + if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) { + results << response(zwave.batteryV1.batteryGet()) + } + break + default: + results << createEvent(displayed: true, descriptionText: "Alarm $cmd.alarmType ${cmd.alarmLevel == 255 ? 'activated' : cmd.alarmLevel ?: 'deactivated'}".toString()) + break + } +} + +// SensorBinary and SensorAlarm aren't tested, but included to preemptively support future smoke alarms +// +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd, results) { + if (cmd.sensorType == physicalgraph.zwave.commandclasses.SensorBinaryV2.SENSOR_TYPE_SMOKE) { + createSmokeEvents(cmd.sensorValue ? "smoke" : "smokeClear", results) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, results) { + if (cmd.sensorType == 1) { + createSmokeEvents(cmd.sensorState ? "smoke" : "smokeClear", results) + } + +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { + results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) { + results << response(zwave.batteryV1.batteryGet(), "delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation()) + } else { + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) { + def map = [ name: "battery", unit: "%", isStateChange: true ] + state.lastbatt = now() + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName battery is low!" + } else { + map.value = cmd.batteryLevel + } + results << createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, results) { + def event = [ displayed: false ] + event.linkText = device.label ?: device.name + event.descriptionText = "$event.linkText: $cmd" + results << createEvent(event) +} diff --git a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy index 3acaa8e..6d3f7b5 100644 --- a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy @@ -21,8 +21,6 @@ metadata { attribute "alarmState", "string" - fingerprint deviceId: "0xA100", inClusters: "0x20,0x80,0x70,0x85,0x71,0x72,0x86" - fingerprint mfr:"0138", prod:"0001", model:"0001", deviceJoinName: "First Alert Smoke Detector" fingerprint mfr:"0138", prod:"0001", model:"0002", deviceJoinName: "First Alert Smoke Detector and Carbon Monoxide Alarm (ZCOMBO)" } @@ -57,6 +55,10 @@ metadata { def installed() { // Device checks in every hour, this interval allows us to miss one check-in notification before marking offline sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + def cmds = [] + createSmokeOrCOEvents("allClear", cmds) // allClear to set inital states for smoke and CO + cmds.each { cmd -> sendEvent(cmd) } } def updated() { @@ -105,6 +107,12 @@ def createSmokeOrCOEvents(name, results) { results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) name = "clear" break + case "allClear": + text = "$device.displayName all clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) + name = "clear" + break case "testClear": text = "$device.displayName test cleared" results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) diff --git a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy index f7f9ad6..7db5ebc 100644 --- a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy +++ b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy @@ -2,26 +2,11 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; -/** - * OpenT2T SmartApp Test - * - * Copyright 2016 OpenT2T - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License - * for the specific language governing permissions and limitations under the License. - * - */ definition( name: "OpenT2T SmartApp Test", namespace: "opent2t", - author: "OpenT2T", - description: "Test app to test end to end SmartThings scenarios via OpenT2T", + author: "Microsoft", + description: "SmartApp for end to end SmartThings scenarios via OpenT2T", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", @@ -55,16 +40,16 @@ definition( //Device Inputs preferences { - section("Allow OpenT2T to control these things...") { - input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false, hideWhenEmpty: true - input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false, hideWhenEmpty: true - input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false, hideWhenEmpty: true - input "cameras", "capability.videoCapture", title: "Which Cameras?", multiple: true, required: false, hideWhenEmpty: true - input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false, hideWhenEmpty: true - input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors", multiple: true, required: false, hideWhenEmpty: true + section("Allow Microsoft to control these things...") { +// input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false, hideWhenEmpty: true +// input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false, hideWhenEmpty: true +// input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false, hideWhenEmpty: true +// input "cameras", "capability.videoCapture", title: "Which Cameras?", multiple: true, required: false, hideWhenEmpty: true +// input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false, hideWhenEmpty: true +// input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors", multiple: true, required: false, hideWhenEmpty: true input "switches", "capability.switch", title: "Which Switches and Lights?", multiple: true, required: false, hideWhenEmpty: true input "thermostats", "capability.thermostat", title: "Which Thermostat?", multiple: true, required: false, hideWhenEmpty: true - input "waterSensors", "capability.waterSensor", title: "Which Water Leak Sensors?", multiple: true, required: false, hideWhenEmpty: true +// input "waterSensors", "capability.waterSensor", title: "Which Water Leak Sensors?", multiple: true, required: false, hideWhenEmpty: true } } @@ -82,36 +67,32 @@ def getInputs() { return inputList } + //API external Endpoints mappings { path("/devices") { - action: - [ + action: [ GET: "getDevices" ] } path("/devices/:id") { - action: - [ + action: [ GET: "getDevice" ] } path("/update/:id") { - action: - [ + action: [ PUT: "updateDevice" ] } path("/deviceSubscription") { - action: - [ + action: [ POST : "registerDeviceChange", DELETE: "unregisterDeviceChange" ] } path("/locationSubscription") { - action: - [ + action: [ POST : "registerDeviceGraph", DELETE: "unregisterDeviceGraph" ] @@ -196,7 +177,7 @@ def registerDeviceChange() { log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" } else if (!state.deviceSubscriptionMap[deviceId].contains(subscriptionEndpt)) { // state.deviceSubscriptionMap[deviceId] << subscriptionEndpt - // For now, we will only have one subscription endpoint per device + // For now, we will only have one subscription endpoint per device state.deviceSubscriptionMap.remove(deviceId) state.deviceSubscriptionMap.put(deviceId, [subscriptionEndpt]) log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" @@ -311,16 +292,16 @@ def deviceEventHandler(evt) { def evtDeviceType = getDeviceType(evtDevice) def deviceData = []; - if (evt.data != null) { - def evtData = parseJson(evt.data) - log.info "Received event for ${evtDevice.displayName}, data: ${evtData}, description: ${evt.descriptionText}" - } - if (evtDeviceType == "thermostat") { deviceData = [name: evtDevice.displayName, id: evtDevice.id, status: evtDevice.status, deviceType: evtDeviceType, manufacturer: evtDevice.manufacturerName, model: evtDevice.modelName, attributes: deviceAttributeList(evtDevice, evtDeviceType), locationMode: getLocationModeInfo(), locationId: location.id] } else { deviceData = [name: evtDevice.displayName, id: evtDevice.id, status: evtDevice.status, deviceType: evtDeviceType, manufacturer: evtDevice.manufacturerName, model: evtDevice.modelName, attributes: deviceAttributeList(evtDevice, evtDeviceType), locationId: location.id] } + + if(evt.data != null){ + def evtData = parseJson(evt.data) + log.info "Received event for ${evtDevice.displayName}, data: ${evtData}, description: ${evt.descriptionText}" + } def params = [body: deviceData] @@ -330,10 +311,10 @@ def deviceEventHandler(evt) { params.uri = "${it}" if (state.verificationKeyMap[it] != null) { def key = state.verificationKeyMap[it] - params.header = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] + params.headers = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] } log.trace "POST URI: ${params.uri}" - log.trace "Header: ${params.header}" + log.trace "Headers: ${params.headers}" log.trace "Payload: ${params.body}" try { httpPostJson(params) { resp -> @@ -363,10 +344,10 @@ def locationEventHandler(evt) { params.uri = "${it}" if (state.verificationKeyMap[it] != null) { def key = state.verificationKeyMap[it] - params.header = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] + params.headers = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] } log.trace "POST URI: ${params.uri}" - log.trace "Header: ${params.header}" + log.trace "Headers: ${params.headers}" log.trace "Payload: ${params.body}" try { httpPostJson(params) { resp -> @@ -385,6 +366,7 @@ def locationEventHandler(evt) { private ComputHMACValue(key, data) { try { + log.debug "data hased: ${data}" SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA1") Mac mac = Mac.getInstance("HmacSHA1") mac.init(secretKeySpec) @@ -507,7 +489,8 @@ private getDeviceType(device) { //Loop through the device capability list to determine the device type. capabilities.each { capability -> - switch (capability.name.toLowerCase()) { + switch(capability.name.toLowerCase()) + { case "switch": deviceType = "switch" @@ -652,7 +635,8 @@ private mapDeviceCommands(command, value) { if (value == 1 || value == "1" || value == "lock") { resultCommand = "lock" resultValue = "" - } else if (value == 0 || value == "0" || value == "unlock") { + } + else if (value == 0 || value == "0" || value == "unlock") { resultCommand = "unlock" resultValue = "" }