diff --git a/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy b/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy new file mode 100644 index 0000000..f86774d --- /dev/null +++ b/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy @@ -0,0 +1,426 @@ +// Express Controls EZMultiPli Multi-sensor +// Motion Sensor - Temperature - Light level - 8 Color Indicator LED - Z-Wave Range Extender - Wall Powered +// driver for SmartThings +// The EZMultiPli is also known as the HSM200 from HomeSeer.com +// +// 2017-04-10 - DrZwave (with help from Don Kirker) - changed fingerprint to the new format, lowered the OnTime +// and other parameters to be "more in line with ST user expectations", get the luminance in LUX so it reports in lux all the time. +// 2016-10-06 - erocm1231 - Added "updated" method to run when configuration options are changed. Depending on model of unit, luminance is being +// reported as a relative percentace or as a lux value. Added the option to configure this in the handler. +// 2016-01-28 - erocm1231 - Changed the configuration method to use scaledConfiguration so that it properly formatted negative numbers. +// Also, added configurationGet and a configurationReport method so that config values can be verified. +// 2015-12-04 - erocm1231 - added range value to preferences as suggested by @Dela-Rick. +// 2015-11-26 - erocm1231 - Fixed null condition error when adding as a new device. +// 2015-11-24 - erocm1231 - Added refresh command. Made a few changes to how the handler maps colors to the LEDs. Fixed +// the device not having its on/off status updated when colors are changed. +// 2015-11-23 - erocm1231 - Changed the look to match SmartThings v2 devices. +// 2015-11-21 - erocm1231 - Made code much more efficient. Also made it compatible when setColor is passed a hex value. +// Mapping of special colors: Soft White - Default - Yellow, White - Concentrate - White, +// Daylight - Energize - Teal, Warm White - Relax - Yellow +// 2015-11-19 - erocm1231 - Fixed a couple incorrect colors, changed setColor to be more compatible with other apps +// 2015-11-18 - erocm1231 - Added to setColor for compatibility with Smart Lighting +// v0.1.0 - DrZWave - chose better icons, Got color LED to work - first fully functional version +// v0.0.9 - jrs - got the temp and luminance to work. Motion works. Debugging the color wheel. +// v0.0.8 - DrZWave 2/25/2015 - change the color control to be tiles since there are only 8 colors. +// v0.0.7 - jrs - 02/23/2015 - Jim Sulin + +metadata { + definition (name: "EZmultiPli", namespace: "DrZWave", author: "Eric Ryherd", oauth: true) { + capability "Actuator" + capability "Sensor" + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Illuminance Measurement" + capability "Switch" + capability "Color Control" + capability "Configuration" + capability "Refresh" + + fingerprint mfr: "001E", prod: "0004", model: "0001" // new format for Fingerprint which is unique for every certified Z-Wave product + } // end definition + + simulator { + // messages the device returns in response to commands it receives + status "motion" : "command: 7105000000FF07, payload: 07" + status "no motion" : "command: 7105000000FF07, payload: 00" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + for (int i = 0; i <= 100; i += 20) { + status "luminance ${i} %": new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() + } + + } //end simulator + + + tiles (scale: 2){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL", icon: "st.Lighting.light18") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', icon:"st.switches.light.on", backgroundColor:"#79b821" + attributeState "turningOff", label:'${name}', icon:"st.switches.light.off", backgroundColor:"#ffffff" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + tileAttribute ("statusText", key: "SECONDARY_CONTROL") { + attributeState "statusText", label:'${currentValue}' + } + } + + standardTile("motion", "device.motion", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true) { + state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" + state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state "temperature", label:'${currentValue}°', unit:"F", icon:"", // would be better if the units would switch to the desired units of the system (imperial or metric) + backgroundColors:[ + [value: 0, color: "#1010ff"], // blue=cold + [value: 65, color: "#a0a0f0"], + [value: 70, color: "#e0e050"], + [value: 75, color: "#f0d030"], // yellow + [value: 80, color: "#fbf020"], + [value: 85, color: "#fbdc01"], + [value: 90, color: "#fb3a01"], + [value: 95, color: "#fb0801"] // red=hot + ] + } + + // icons to use would be st.Weather.weather2 or st.alarm.temperature.normal - see http://scripts.3dgo.net/smartthings/icons/ for a list of icons + valueTile("illuminance", "device.illuminance", width: 2, height: 2, inactiveLabel: false) { +// jrs 4/7/2015 - Null on display + //state "luminosity", label:'${currentValue} ${unit}' + state "luminosity", label:'${currentValue}', unit:'${currentValue}', icon:"", + backgroundColors:[ + [value: 25, color: "#404040"], + [value: 50, color: "#808080"], + [value: 75, color: "#a0a0a0"], + [value: 90, color: "#e0e0e0"], + //lux measurement values + [value: 150, color: "#404040"], + [value: 300, color: "#808080"], + [value: 600, color: "#a0a0a0"], + [value: 900, color: "#e0e0e0"] + ] + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main (["temperature","motion", "switch"]) + details(["switch", "motion", "temperature", "illuminance", "refresh", "configure"]) + } // end tiles + + preferences { + input "OnTime", "number", title: "No Motion Interval", description: "N minutes lights stay on after no motion detected [0, 1-127]", range: "0..127", defaultValue: 2, displayDuringSetup: true, required: false + input "OnLevel", "number", title: "Dimmer Onlevel", description: "Dimmer OnLevel for associated node 2 lights [-1, 0, 1-99]", range: "-1..99", defaultValue: -1, displayDuringSetup: true, required: false + input "LiteMin", "number", title: "Luminance Report Frequency", description: "Luminance report sent every N minutes [0-127]", range: "0..127", defaultValue: 6, displayDuringSetup: true, required: false + input "TempMin", "number", title: "Temperature Report Frequency", description: "Temperature report sent every N minutes [0-127]", range: "0..127", defaultValue: 6, displayDuringSetup: true, required: false + input "TempAdj", "number", title: "Temperature Calibration", description: "Adjust temperature up/down N tenths of a degree F [(-127)-(+128)]", range: "-127..128", defaultValue: 0, displayDuringSetup: true, required: false + input("lum", "enum", title:"Illuminance Measurement", description: "Percent or Lux", defaultValue: 2 ,required: false, displayDuringSetup: true, options: + [1:"Percent", + 2:"Lux"]) + } + +} // end metadata + + +// Parse incoming device messages from device to generate events +def parse(String description){ + //log.debug "==> New Zwave Event: ${description}" + def result = [] + def cmd = zwave.parse(description, [0x31: 5]) // 0x31=SensorMultilevel which we force to be version 5 + if (cmd) { + def cmdData = zwaveEvent(cmd) + if (cmdData != [:]) + result << createEvent(cmdData) + } + + def statusTextmsg = "" + if (device.currentState('temperature') != null && device.currentState('illuminance') != null) { + statusTextmsg = "${device.currentState('temperature').value} ° - ${device.currentState('illuminance').value} ${(lum == "" || lum == null || lum == 1) ? "%" : "LUX"}" + sendEvent("name":"statusText", "value":statusTextmsg, displayed:false) + } + if (result != [null] && result != []) log.debug "Parse returned ${result}" + + + return result +} + + +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ + def map = [:] + switch (cmd.sensorType) { + case 0x01: // SENSOR_TYPE_TEMPERATURE_VERSION_1 + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + log.debug "Temperature report" + break; + case 0x03 : // SENSOR_TYPE_LUMINANCE_VERSION_1 + map.value = cmd.scaledSensorValue.toInteger().toString() + if(lum == "" || lum == null || lum == 1) map.unit = "%" + else map.unit = "lux" + map.name = "illuminance" + log.debug "Luminance report" + break; + } + return map +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def map = [:] + if (cmd.notificationType==0x07) { // NOTIFICATION_TYPE_BURGLAR + if (cmd.event==0x07 || cmd.event==0x08) { + map.name = "motion" + map.value = "active" + map.descriptionText = "$device.displayName motion detected" + log.debug "motion recognized" + } else if (cmd.event==0) { + map.name = "motion" + map.value = "inactive" + map.descriptionText = "$device.displayName no motion detected" + log.debug "No motion recognized" + } + } + if (map.name != "motion") { + log.debug "unmatched parameters for cmd: ${cmd.toString()}}" + } + return map +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + if (cmd.value == 0 && device.latestState("color").value != "#ffffff") { + sendEvent(name: "color", value: "#ffffff", displayed: true) + } + [name: "switch", value: cmd.value ? "on" : "off", type: "digital"] +} + +def updated() +{ + log.debug "updated() is being called" + + def cmds = configure() + + if (cmds != []) response(cmds) +} + +def on() { + log.debug "Turning Light 'on'" + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.basicV1.basicGet().format() + ], 500) +} + +def off() { + log.debug "Turning Light 'off'" + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.basicV1.basicGet().format() + ], 500) +} + + +def setColor(value) { + log.debug "setColor() : ${value}" + def myred + def mygreen + def myblue + def hexValue + def cmds = [] + + if ( value.level == 1 && value.saturation > 20) { + def rgb = huesatToRGB(value.hue as Integer, 100) + myred = rgb[0] >=128 ? 255 : 0 + mygreen = rgb[1] >=128 ? 255 : 0 + myblue = rgb[2] >=128 ? 255 : 0 + } + else if ( value.level > 1 ) { + def rgb = huesatToRGB(value.hue as Integer, value.saturation as Integer) + myred = rgb[0] >=128 ? 255 : 0 + mygreen = rgb[1] >=128 ? 255 : 0 + myblue = rgb[2] >=128 ? 255 : 0 + } + else if (value.hex) { + def rgb = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + myred = rgb[0] >=128 ? 255 : 0 + mygreen = rgb[1] >=128 ? 255 : 0 + myblue = rgb[2] >=128 ? 255 : 0 + } + else { + myred=value.red >=128 ? 255 : 0 // the EZMultiPli has just on/off for each of the 3 channels RGB so convert the 0-255 value into 0 or 255. + mygreen=value.green >=128 ? 255 : 0 + myblue=value.blue>=128 ? 255 : 0 + } + //log.debug "Red: ${myred} Green: ${mygreen} Blue: ${myblue}" + //cmds << zwave.colorControlV1.stateSet(stateDataLength: 3, VariantGroup1: [0x02, myred], VariantGroup2:[ 0x03, mygreen], VariantGroup3:[0x04,myblue]).format() // ST support for this command as of 2015/02/23 does not support the color IDs so this command cannot be used. + // So instead we'll use these commands to hack around the lack of support of the above command + cmds << zwave.basicV1.basicSet(value: 0x00).format() // As of 2015/02/23 ST is not supporting stateSet properly but found this hack that works. + if (myred!=0) { + cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x02, startState: myred, ignoreStartState: True, updown: True).format() + cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x02).format() + } + if (mygreen!=0) { + cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x03, startState: mygreen, ignoreStartState: True, updown: True).format() + cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x03).format() + } + if (myblue!=0) { + cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x04, startState: myblue, ignoreStartState: True, updown: True).format() + cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x04).format() + } + cmds << zwave.basicV1.basicGet().format() + hexValue = rgbToHex([r:myred, g:mygreen, b:myblue]) + if(hexValue) sendEvent(name: "color", value: hexValue, displayed: true) + delayBetween(cmds, 100) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +// ensure we are passing acceptable param values for LiteMin & TempMin configs +def checkLiteTempInput(value) { + if (value == null) { + value=60 + } + def liteTempVal = value.toInteger() + switch (liteTempVal) { + case { it < 0 }: + return 60 // bad value, set to default + break + case { it > 127 }: + return 127 // bad value, greater then MAX, set to MAX + break + default: + return liteTempVal // acceptable value + } +} + +// ensure we are passing acceptable param value for OnTime config +def checkOnTimeInput(value) { + if (value == null) { + value=10 + } + def onTimeVal = value.toInteger() + switch (onTimeVal) { + case { it < 0 }: + return 10 // bad value set to default + break + case { it > 127 }: + return 127 // bad value, greater then MAX, set to MAX + break + default: + return onTimeVal // acceptable value + } +} + +// ensure we are passing acceptable param value for OnLevel config +def checkOnLevelInput(value) { + if (value == null) { + value=99 + } + def onLevelVal = value.toInteger() + switch (onLevelVal) { + case { it < -1 }: + return -1 // bad value set to default + break + case { it > 99 }: + return 99 // bad value, greater then MAX, set to MAX + break + default: + return onLevelVal // acceptable value + } +} + + +// ensure we are passing an acceptable param value for TempAdj configs +def checkTempAdjInput(value) { + if (value == null) { + value=0 + } + def tempAdjVal = value.toInteger() + switch (tempAdjVal) { + case { it < -127 }: + return 0 // bad value, set to default + break + case { it > 128 }: + return 128 // bad value, greater then MAX, set to MAX + break + default: + return tempAdjVal // acceptable value + } +} + +def refresh() { + def cmd = [] + cmd << zwave.switchColorV3.switchColorGet().format() + cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1).format() + cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1).format() + cmd << zwave.basicV1.basicGet().format() + delayBetween(cmd, 1000) +} + +def configure() { + log.debug "OnTime=${settings.OnTime} OnLevel=${settings.OnLevel} TempAdj=${settings.TempAdj}" + def cmd = delayBetween([ + zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, scaledConfigurationValue: checkOnTimeInput(settings.OnTime)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: checkOnLevelInput(settings.OnLevel)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: checkLiteTempInput(settings.LiteMin)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: checkLiteTempInput(settings.TempMin)).format(), + zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: checkTempAdjInput(settings.TempAdj)).format(), + zwave.configurationV1.configurationGet(parameterNumber: 1).format(), + zwave.configurationV1.configurationGet(parameterNumber: 2).format(), + zwave.configurationV1.configurationGet(parameterNumber: 3).format(), + zwave.configurationV1.configurationGet(parameterNumber: 4).format(), + zwave.configurationV1.configurationGet(parameterNumber: 5).format() + ], 100) + //log.debug cmd + cmd + refresh() +} + +def huesatToRGB(float hue, float sat) { + while(hue >= 100) hue -= 100 + int h = (int)(hue / 100 * 6) + float f = hue / 100 * 6 - h + int p = Math.round(255 * (1 - (sat / 100))) + int q = Math.round(255 * (1 - (sat / 100) * f)) + int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) + switch (h) { + case 0: return [255, t, p] + case 1: return [q, 255, p] + case 2: return [p, 255, t] + case 3: return [p, q, 255] + case 4: return [t, p, 255] + case 5: return [255, p, q] + } +} +def rgbToHex(rgb) { + def r = hex(rgb.r) + def g = hex(rgb.g) + def b = hex(rgb.b) + def hexColor = "#${r}${g}${b}" + + hexColor +} +private hex(value, width=2) { + def s = new BigInteger(Math.round(value).toString()).toString(16) + while (s.size() < width) { + s = "0" + s + } + s +} diff --git a/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy b/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy index 9498e69..c434d3d 100644 --- a/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy +++ b/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings + * Copyright 2017 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: @@ -59,7 +59,7 @@ def updated() { } def configure() { - def cmds = zigbee.batteryConfig(20, 20, 0x01) + def cmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + zigbee.batteryConfig(20, 20, 0x01) log.debug "configure -- cmds: ${cmds}" return cmds } @@ -166,4 +166,4 @@ def checkPresenceCallback() { if (timeSinceLastCheckin >= theCheckInterval) { handlePresenceEvent(false) } -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy index 370ecb7..7c658c1 100644 --- a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy +++ b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy @@ -23,9 +23,9 @@ metadata { capability "Health Check" capability "Light" - fingerprint mfr:"0063", prod:"4457", deviceJoinName: "GE In-Wall Smart Dimmer " - fingerprint mfr:"0063", prod:"4944", deviceJoinName: "GE In-Wall Smart Dimmer " - fingerprint mfr:"0063", prod:"5044", deviceJoinName: "GE Plug-In Smart Dimmer " + fingerprint mfr:"0063", prod:"4457", deviceJoinName: "GE In-Wall Smart Dimmer" + fingerprint mfr:"0063", prod:"4944", deviceJoinName: "GE In-Wall Smart Dimmer" + fingerprint mfr:"0063", prod:"5044", deviceJoinName: "GE Plug-In Smart Dimmer" fingerprint mfr:"0063", prod:"4944", model:"3034", deviceJoinName: "GE In-Wall Smart Fan Control" } diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 2579423..2b28b6c 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -114,9 +114,17 @@ def refresh() { def configure() { log.debug "Configuring Reporting and Bindings." - + def cmds = [] + if (device.getDataValue("manufacturer") == "sengled") { + def startLevel = 0xFE + if ((device.currentState("level")?.value != null)) { + startLevel = Math.round(Integer.parseInt(device.currentState("level").value) * 0xFE / 100) + } + // Level Control Cluster, command Move to Level, level start level, transition time 0 + cmds << zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x00, sprintf("%02X0000", startLevel)) + } // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) // enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - refresh() + cmds + refresh() } diff --git a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy index 7e448da..4edce26 100644 --- a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy @@ -30,6 +30,9 @@ metadata { fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82" fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters:"0x20" // Philio multi+ fingerprint mfr:"0086", prod:"0002", model:"001D", deviceJoinName: "Aeon Labs Door/Window Sensor (Gen 5)" + fingerprint mfr:"014A", prod:"0001", model:"0002", deviceJoinName: "Ecolink Door/Window Sensor" + fingerprint mfr:"0086", prod:"0102", model:"0070", deviceJoinName: "Aeon Labs Door/Window Sensor 6" + fingerprint mfr:"011A", prod:"0601", model:"0903", deviceJoinName: "Enerwave Magnetic Door/Window Sensor" } // simulator metadata diff --git a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy index 0406a1e..7db5f5c 100644 --- a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy +++ b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy @@ -22,9 +22,9 @@ metadata { capability "Health Check" capability "Light" - fingerprint mfr:"0063", prod:"4952", deviceJoinName: "Z-Wave Wall Switch" - fingerprint mfr:"0063", prod:"5257", deviceJoinName: "Z-Wave Wall Switch" - fingerprint mfr:"0063", prod:"5052", deviceJoinName: "Z-Wave Plug-In Switch" + fingerprint mfr:"0063", prod:"4952", deviceJoinName: "GE Wall Switch" + fingerprint mfr:"0063", prod:"5257", deviceJoinName: "GE Wall Switch" + fingerprint mfr:"0063", prod:"5052", deviceJoinName: "GE Plug-In Switch" fingerprint mfr:"0113", prod:"5257", deviceJoinName: "Z-Wave Wall Switch" } 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 91ddaef..845e8db 100644 --- a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy +++ b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy @@ -52,15 +52,15 @@ definition( //Device Inputs preferences { section("Allow OpenT2T to control these things...") { - input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false - input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false - input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false - input "cameras", "capability.videoCapture", title: "Which Cameras?", multiple: true, required: false - input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false - input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors", multiple: true, required: false - input "switches", "capability.switch", title: "Which Switches and Lights?", multiple: true, required: false - input "thermostats", "capability.thermostat", title: "Which Thermostat?", multiple: true, required: false - input "waterSensors", "capability.waterSensor", title: "Which Water Leak Sensors?", multiple: true, required: false + 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 } } @@ -80,16 +80,6 @@ def getInputs() { //API external Endpoints mappings { - path("/subscriptionURL/:url") { - action: [ - PUT: "updateEndpointURL" - ] - } - path("/connectionId/:connId") { - action: [ - PUT: "updateConnectionId" - ] - } path("/devices") { action: [ GET: "getDevices" @@ -105,33 +95,52 @@ mappings { PUT: "updateDevice" ] } - path("/subscription/:id") { + path("/deviceSubscription") { action: [ POST: "registerDeviceChange", DELETE: "unregisterDeviceChange" ] } + path("/locationSubscription") { + action: [ + POST: "registerDeviceGraph", + DELETE: "unregisterDeviceGraph" + ] + } } def installed() { - log.debug "Installed with settings: ${settings}" + log.debug "Installing with settings: ${settings}" initialize() } def updated() { - log.debug "Updated with settings: ${settings}" + log.debug "Updating with settings: ${settings}" + if(state.deviceSubscriptionMap == null){ + state.deviceSubscriptionMap = [:] + log.debug "deviceSubscriptionMap created." + } + if( state.locationSubscriptionMap == null){ + state.locationSubscriptionMap = [:] + log.debug "locationSubscriptionMap created." + } unsubscribe() - registerSubscriptions() + registerAllDeviceSubscriptions() } def initialize() { - state.connectionId = "" - state.endpointURL = "https://ifs.windows-int.com/v1/cb/81C7E77B-EABC-488A-B2BF-FEC42F0DABD2/notify" - registerSubscriptions() + log.debug "Initializing with settings: ${settings}" + state.deviceSubscriptionMap = [:] + log.debug "deviceSubscriptionMap created." + registerAllDeviceSubscriptions() + state.locationSubscriptionMap = [:] + log.debug "locationSubscriptionMap created." } +/*** Subscription Functions ***/ + //Subscribe events for all devices -def registerSubscriptions() { +def registerAllDeviceSubscriptions() { registerChangeHandler(inputs) } @@ -140,101 +149,195 @@ def registerChangeHandler(myList) { myList.each { myDevice -> def theAtts = myDevice.supportedAttributes theAtts.each {att -> - subscribe(myDevice, att.name, eventHandler) - log.info "Registering ${myDevice.displayName}.${att.name}" + subscribe(myDevice, att.name, deviceEventHandler) + log.info "Registering for ${myDevice.displayName}.${att.name}" } } } //Endpoints function: Subscribe to events from a specific device def registerDeviceChange() { - def myDevice = findDevice(params.id) + def subscriptionEndpt = params.subscriptionURL + def deviceId = params.deviceId + def myDevice = findDevice(deviceId) + if( myDevice == null ){ + httpError(404, "Cannot find device with device ID ${deviceId}.") + } + def theAtts = myDevice.supportedAttributes try { theAtts.each {att -> - subscribe(myDevice, att.name, eventHandler) - log.info "Registering ${myDevice.displayName}.${att.name}" + subscribe(myDevice, att.name, deviceEventHandler) + } + log.info "Subscribing for ${myDevice.displayName}" + + if(subscriptionEndpt != null){ + if(state.deviceSubscriptionMap[deviceId] == null){ + state.deviceSubscriptionMap.put(deviceId, [subscriptionEndpt]) + log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" + } else if (!state.deviceSubscriptionMap[deviceId].contains(subscriptionEndpt)){ + state.deviceSubscriptionMap[deviceId] << subscriptionEndpt + log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" + } } - return ["succeed"] } catch (e) { httpError(500, "something went wrong: $e") } + + log.info "Current subscription map is ${state.deviceSubscriptionMap}" + return ["succeed"] } //Endpoints function: Unsubscribe to events from a specific device def unregisterDeviceChange() { - def myDevice = findDevice(params.id) + def subscriptionEndpt = params.subscriptionURL + def deviceId = params.deviceId + def myDevice = findDevice(deviceId) + + if( myDevice == null ){ + httpError(404, "Cannot find device with device ID ${deviceId}.") + } + try { - unsubscribe(myDevice) - log.info "Unregistering ${myDevice.displayName}" - return ["succeed"] + if(subscriptionEndpt != null && subscriptionEndpt != "undefined"){ + if (state.deviceSubscriptionMap[deviceId]?.contains(subscriptionEndpt)){ + if(state.deviceSubscriptionMap[deviceId].size() == 1){ + state.deviceSubscriptionMap.remove(deviceId) + } else { + state.deviceSubscriptionMap[deviceId].remove(subscriptionEndpt) + } + log.info "Removed subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" + } + } else { + state.deviceSubscriptionMap.remove(deviceId) + log.info "Unsubscriping for ${myDevice.displayName}" + } } catch (e) { httpError(500, "something went wrong: $e") } + + log.info "Current subscription map is ${state.deviceSubscriptionMap}" +} + +//Endpoints function: Subscribe to device additiona/removal updated in a location +def registerDeviceGraph() { + def subscriptionEndpt = params.subscriptionURL + + if (subscriptionEndpt != null && subscriptionEndpt != "undefined"){ + subscribe(location, "DeviceCreated", locationEventHandler, [filterEvents: false]) + subscribe(location, "DeviceUpdated", locationEventHandler, [filterEvents: false]) + subscribe(location, "DeviceDeleted", locationEventHandler, [filterEvents: false]) + + if(state.locationSubscriptionMap[location.id] == null){ + state.locationSubscriptionMap.put(location.id, [subscriptionEndpt]) + log.info "Added subscription URL: ${subscriptionEndpt} for Location ${location.name}" + } else if (!state.locationSubscriptionMap[location.id].contains(subscriptionEndpt)){ + state.locationSubscriptionMap[location.id] << subscriptionEndpt + log.info "Added subscription URL: ${subscriptionEndpt} for Location ${location.name}" + } + log.info "Current location subscription map is ${state.locationSubscriptionMap}" + return ["succeed"] + } else { + httpError(400, "missing input parameter: subscriptionURL") + } +} + +//Endpoints function: Unsubscribe to events from a specific device +def unregisterDeviceGraph() { + def subscriptionEndpt = params.subscriptionURL + + try { + if(subscriptionEndpt != null && subscriptionEndpt != "undefined"){ + if (state.locationSubscriptionMap[location.id]?.contains(subscriptionEndpt)){ + if(state.locationSubscriptionMap[location.id].size() == 1){ + state.locationSubscriptionMap.remove(location.id) + } else { + state.locationSubscriptionMap[location.id].remove(subscriptionEndpt) + } + log.info "Removed subscription URL: ${subscriptionEndpt} for Location ${location.name}" + } + }else{ + httpError(400, "missing input parameter: subscriptionURL") + } + } catch (e) { + httpError(500, "something went wrong: $e") + } + + log.info "Current location subscription map is ${state.locationSubscriptionMap}" } //When events are triggered, send HTTP post to web socket servers -def eventHandler(evt) { - def evt_device_id = evt.deviceId - def evt_device_value = evt.value - def evt_name = evt.name +def deviceEventHandler(evt) { def evt_device = evt.device - def evt_deviceType = getDeviceType(evt_device); + def evt_deviceType = getDeviceType(evt_device) def deviceInfo - if(evt_deviceType == "thermostat") - { - deviceInfo = [name: evt_device.displayName, id: evt_device.id, status:evt_device.getStatus(), deviceType:evt_deviceType, manufacturer:evt_device.getManufacturerName(), model:evt_device.getModelName(), attributes: deviceAttributeList(evt_device), locationMode: getLocationModeInfo()] - } - else - { - deviceInfo = [name: evt_device.displayName, id: evt_device.id, status:evt_device.getStatus(), deviceType:evt_deviceType, manufacturer:evt_device.getManufacturerName(), model:evt_device.getModelName(), attributes: deviceAttributeList(evt_device)] + def params = [ body: [deviceName: evt_device.displayName, deviceId: evt_device.id, locationId: location.id] ] + + if(evt.data != null){ + def evtData = parseJson(evt.data) + log.info "Received event for ${evt_device.displayName}, data: ${evtData}, description: ${evt.descriptionText}" } - def params = [ - uri: "${state.endpointURL}/${state.connectionId}", - body: [ deviceInfo ] - ] - try { + //send event to all subscriptions urls + log.debug "Current subscription urls for ${evt_device.displayName} is ${state.deviceSubscriptionMap[evt_device.id]}" + state.deviceSubscriptionMap[evt_device.id].each { + params.uri = "${it}" log.trace "POST URI: ${params.uri}" log.trace "Payload: ${params.body}" - httpPostJson(params) { resp -> - resp.headers.each { - log.debug "${it.name} : ${it.value}" + try{ + httpPostJson(params) { resp -> + log.trace "response status code: ${resp.status}" + log.trace "response data: ${resp.data}" } - log.trace "response status code: ${resp.status}" - log.trace "response data: ${resp.data}" + } catch (e) { + log.error "something went wrong: $e" } - } catch (e) { - log.debug "something went wrong: $e" } } -//Endpoints function: update subcription endpoint url [state.endpoint] -void updateEndpointURL() { - state.endpointURL = params.url - log.info "Updated EndpointURL to ${state.endpointURL}" +def locationEventHandler(evt) { + log.info "Received event for location ${location.name}/${location.id}, Event: ${evt.name}, description: ${evt.descriptionText}, apiServerUrl: ${apiServerUrl("")}" + switch(evt.name){ + case "DeviceCreated": + case "DeviceDeleted": + def evt_device = evt.device + def evt_deviceType = getDeviceType(evt_device) + log.info "DeviceName: ${evt_device.displayName}, DeviceID: ${evt_device.id}, deviceType: ${evt_deviceType}" + + def params = [ body: [ eventType:evt.name, deviceId: evt_device.id, locationId: location.id ] ] + + state.locationSubscriptionMap[location.id].each { + params.uri = "${it}" + log.trace "POST URI: ${params.uri}" + log.trace "Payload: ${params.body}" + try{ + httpPostJson(params) { resp -> + log.trace "response status code: ${resp.status}" + log.trace "response data: ${resp.data}" + } + } catch (e) { + log.error "something went wrong: $e" + } + } + case "DeviceUpdated": + default: + break + } } -//Endpoints function: update global variable [state.connectionId] -void updateConnectionId() { - def connId = params.connId - state.connectionId = connId - log.info "Updated ConnectionID to ${state.connectionId}" -} + +/*** Device Query/Update Functions ***/ //Endpoints function: return all device data in json format def getDevices() { def deviceData = [] inputs?.each { def deviceType = getDeviceType(it) - if(deviceType == "thermostat") - { - deviceData << [name: it.displayName, id: it.id, status:it.getStatus(), deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it), locationMode: getLocationModeInfo()] - } - else - { - deviceData << [name: it.displayName, id: it.id, status:it.getStatus(), deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it)] + if(deviceType == "thermostat") { + deviceData << [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it, deviceType), locationMode: getLocationModeInfo()] + } else { + deviceData << [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it, deviceType)] } } @@ -247,14 +350,12 @@ def getDevice() { def it = findDevice(params.id) def deviceType = getDeviceType(it) def device - if(deviceType == "thermostat") - { - device = [name: it.displayName, id: it.id, status:it.getStatus(), deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it), locationMode: getLocationModeInfo()] - } - else - { - device = [name: it.displayName, id: it.id, status:it.getStatus(), deviceType:deviceType, manufacturer:it.getManufacturerName(), model:it.getModelName(), attributes: deviceAttributeList(it)] + if(deviceType == "thermostat") { + device = [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it,deviceType), locationMode: getLocationModeInfo()] + } else { + device = [name: it.displayName, id: it.id, status:it.status, deviceType:deviceType, manufacturer:it.manufacturerName, model:it.modelName, attributes: deviceAttributeList(it, deviceType)] } + log.debug "getDevice, return: ${device}" return device } @@ -350,9 +451,6 @@ private getDeviceType(device) { } } break - case "contact sensor": - deviceType = "contactSensor" - return deviceType case "garageDoorControl": deviceType = "garageDoor" return deviceType @@ -362,17 +460,15 @@ private getDeviceType(device) { case "video camera": deviceType = "camera" return deviceType - case "motion sensor": - deviceType = "motionSensor" - return deviceType - case "presence sensor": - deviceType = "presenceSensor" - return deviceType case "thermostat": deviceType = "thermostat" return deviceType + case "acceleration sensor": + case "contact sensor": + case "motion sensor": + case "presence sensor": case "water sensor": - deviceType = "waterSensor" + deviceType = "genericSensor" return deviceType default: break @@ -387,14 +483,33 @@ private findDevice(deviceId) { } //Return a list of device attributes -private deviceAttributeList(device) { - device.supportedAttributes.collectEntries { attribute-> +private deviceAttributeList(device, deviceType) { + def attributeList = [:] + def allAttributes = device.supportedAttributes + allAttributes.each { attribute -> try { - [ (attribute.name): device.currentValue(attribute.name) ] + def currentState = device.currentState(attribute.name) + if(currentState != null ){ + switch(attribute.name){ + case 'temperature': + attributeList.putAll([ (attribute.name): currentState.value, 'temperatureScale':location.temperatureScale ]) + break; + default: + attributeList.putAll([(attribute.name): currentState.value ]) + break; + } + if( deviceType == "genericSensor" ){ + def key = attribute.name + "_lastUpdated" + attributeList.putAll([ (key): currentState.isoDate ]) + } + } else { + attributeList.putAll([ (attribute.name): null ]); + } } catch(e) { - [ (attribute.name): null ] + attributeList.putAll([ (attribute.name): null ]); } } + return attributeList } //Map device command and value. diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 5ded2b2..b343b20 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -73,7 +73,7 @@ def bridgeDiscovery(params = [:]) { } ssdpSubscribe() - + log.trace "bridgeRefreshCount: $bridgeRefreshCount" //bridge discovery request every 15 //25 seconds if ((bridgeRefreshCount % 5) == 0) { discoverBridges() @@ -207,6 +207,7 @@ def bulbDiscovery() { } private discoverBridges() { + log.trace "Sending Hue Discovery message to the hub" sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN)) }