From 24f63a514a4cc78461c61df5d2ff68b27e3c99a7 Mon Sep 17 00:00:00 2001 From: Bob Florian Date: Wed, 5 Apr 2017 11:40:46 -0700 Subject: [PATCH 1/6] SAPLAT-41 added simulated refrigerator composite device type example --- .../simulated-refrigerator-door.groovy | 57 +++++++++++ ...ed-refrigerator-temperature-control.groovy | 94 +++++++++++++++++++ .../simulated-refrigerator.groovy | 91 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy create mode 100644 devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy create mode 100644 devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy diff --git a/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy b/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy new file mode 100644 index 0000000..cf7f3ef --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy @@ -0,0 +1,57 @@ +/** + * 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: + * + * 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: "Simulated Refrigerator Door", namespace: "smartthings/testing", author: "SmartThings") { + capability "Contact Sensor" + capability "Sensor" + + command "open" + command "close" + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821", action: "open") + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e", action: "close") + } + standardTile("freezerDoor", "device.contact", width: 2, height: 2, decoration: "flat") { + state("closed", label:'Freezer', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + state("open", label:'Freezer', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + } + standardTile("mainDoor", "device.contact", width: 2, height: 2, decoration: "flat") { + state("closed", label:'Fridge', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + state("open", label:'Fridge', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + } + standardTile("control", "device.contact", width: 1, height: 1, decoration: "flat") { + state("closed", label:'${name}', icon:"st.contact.contact.closed", action: "open") + state("open", label:'${name}', icon:"st.contact.contact.open", action: "close") + } + main "contact" + details "contact" + } +} + +def installed() { + sendEvent(name: "contact", value: "closed") +} + +def open() { + sendEvent(name: "contact", value: "open") + parent.doorOpen(device.deviceNetworkId) +} + +def close() { + sendEvent(name: "contact", value: "closed") + parent.doorClosed(device.deviceNetworkId) +} diff --git a/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy b/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy new file mode 100644 index 0000000..972641e --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy @@ -0,0 +1,94 @@ +/** + * Simulated Refrigerator Temperature Control + * + * 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: + * + * 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: "Simulated Refrigerator Temperature Control", namespace: "smartthings/testing", author: "SmartThings") { + capability "Temperature Measurement" + capability "Thermostat Cooling Setpoint" + + command "tempUp" + command "tempDown" + command "setpointUp" + command "setpointDown" + } + + tiles { + valueTile("refrigerator", "device.temperature", width: 2, height: 2, canChangeBackground: true) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 0, color: "#153591"], + [value: 40, color: "#1e9cbb"], + [value: 45, color: "#f1d801"] + ] + ) + } + valueTile("freezer", "device.temperature", width: 2, height: 2, canChangeBackground: true) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 0, color: "#153591"], + [value: 5, color: "#1e9cbb"], + [value: 15, color: "#f1d801"] + ] + ) + } + valueTile("freezerSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "setpoint", label:'Freezer Set: ${currentValue}°', unit:"F" + } + valueTile("refrigeratorSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'Fridge Set: ${currentValue}°', unit:"F" + } + standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", action:"tempUp", icon:"st.thermostat.thermostat-up" + } + standardTile("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", action:"tempDown", icon:"st.thermostat.thermostat-down" + } + standardTile("setpointUp", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "default", action:"setpointUp", icon:"st.thermostat.thermostat-up" + } + standardTile("setpointDown", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "default", action:"setpointDown", icon:"st.thermostat.thermostat-down" + } + } +} + +def installed() { + sendEvent(name: "temperature", value: device.componentName == "freezer" ? 2 : 40) + sendEvent(name: "coolingSetpoint", value: device.componentName == "freezer" ? 2 : 40) +} + +def updated() { + installed() +} + +void tempUp() { + def value = device.currentValue("temperature") as Integer + sendEvent(name: "temperature", value: value + 1) +} + +void tempDown() { + def value = device.currentValue("temperature") as Integer + sendEvent(name: "temperature", value: value - 1) +} + +void setpointUp() { + def value = device.currentValue("coolingSetpoint") as Integer + sendEvent(name: "coolingSetpoint", value: value + 1) +} + +void setpointDown() { + def value = device.currentValue("coolingSetpoint") as Integer + sendEvent(name: "coolingSetpoint", value: value - 1) +} diff --git a/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy b/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy new file mode 100644 index 0000000..ba5fbfe --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy @@ -0,0 +1,91 @@ +/** + * Simulated Refrigerator + * + * Example composite device handler that simulates a refrigerator with a freezer compartment and a main compartment. + * Each of these compartments has its own door, temperature, and temperature setpoint. Each compartment modeled + * as a child device of the main refrigerator device so that temperature-based SmartApps can be used with each + * compartment + * + * 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: + * + * 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: "Simulated Refrigerator", namespace: "smartthings/testing", author: "SmartThings") { + capability "Contact Sensor" + } + + tiles(scale: 2) { + standardTile("contact", "device.contact", width: 4, height: 4) { + state("closed", label:'${name}', icon:"st.fridge.fridge-closed", backgroundColor:"#79b821") + state("open", label:'${name}', icon:"st.fridge.fridge-open", backgroundColor:"#ffa81e") + } + childDeviceTile("freezerDoor", "freezerDoor", height: 2, width: 2, childTileName: "freezerDoor") + childDeviceTile("mainDoor", "mainDoor", height: 2, width: 2, childTileName: "mainDoor") + childDeviceTile("freezer", "freezer", height: 2, width: 2, childTileName: "freezer") + childDeviceTile("refrigerator", "refrigerator", height: 2, width: 2, childTileName: "refrigerator") + childDeviceTile("freezerSetpoint", "freezer", height: 1, width: 2, childTileName: "freezerSetpoint") + childDeviceTile("refrigeratorSetpoint", "refrigerator", height: 1, width: 2, childTileName: "refrigeratorSetpoint") + + // for simulator + childDeviceTile("freezerUp", "freezer", height: 1, width: 1, childTileName: "tempUp") + childDeviceTile("freezerDown", "freezer", height: 1, width: 1, childTileName: "tempDown") + childDeviceTile("refrigeratorUp", "refrigerator", height: 1, width: 1, childTileName: "tempUp") + childDeviceTile("refrigeratorDown", "refrigerator", height: 1, width: 1, childTileName: "tempDown") + childDeviceTile("freezerDoorControl", "freezerDoor", height: 1, width: 1, childTileName: "control") + childDeviceTile("mainDoorControl", "mainDoor", height: 1, width: 1, childTileName: "control") + childDeviceTile("freezerSetpointUp", "freezer", height: 1, width: 1, childTileName: "setpointUp") + childDeviceTile("freezerSetpointDown", "freezer", height: 1, width: 1, childTileName: "setpointDown") + childDeviceTile("refrigeratorSetpointUp", "refrigerator", height: 1, width: 1, childTileName: "setpointUp") + childDeviceTile("refrigeratorSetpointDown", "refrigerator", height: 1, width: 1, childTileName: "setpointDown") + } +} + +def installed() { + state.counter = state.counter ? state.counter + 1 : 1 + if (state.counter == 1) { + addChildDevice( + "Simulated Refrigerator Door", + "${device.deviceNetworkId}.1", + null, + [completedSetup: true, label: "${device.label} (Freezer Door)", componentName: "freezerDoor", componentLabel: "Freezer Door"]) + + addChildDevice( + "Simulated Refrigerator Door", + "${device.deviceNetworkId}.2", + null, + [completedSetup: true, label: "${device.label} (Main Door)", componentName: "mainDoor", componentLabel: "Main Door"]) + + addChildDevice( + "Simulated Refrigerator Temperature Control", + "${device.deviceNetworkId}.3", + null, + [completedSetup: true, label: "${device.label} (Freezer)", componentName: "freezer", componentLabel: "Freezer"]) + + addChildDevice( + "Simulated Refrigerator Temperature Control", + "${device.deviceNetworkId}.3", + null, + [completedSetup: true, label: "${device.label} (Fridge)", componentName: "refrigerator", componentLabel: "Fridge"]) + } +} + +def doorOpen(dni) { + // If any door opens, then the refrigerator is considered to be open + sendEvent(name: "contact", value: "open") +} + +def doorClosed(dni) { + // Both doors must be closed for the refrigerator to be considered closed + if (!childDevices.find{it.deviceNetworkId != dni && it.currentValue("contact") == "open"}) { + sendEvent(name: "contact", value: "closed") + } +} \ No newline at end of file From c65c9cc185f636e5767a169f244a0d797c31dba9 Mon Sep 17 00:00:00 2001 From: Eric Ryherd Date: Tue, 11 Apr 2017 05:25:16 -0700 Subject: [PATCH 2/6] =?UTF-8?q?MSA-1883:=20Features:=20Motion=20Sensor=20-?= =?UTF-8?q?=20=20Passive=20Infrared=20Sensor=20(PIR)=20-=20=2012=E2=80=99?= =?UTF-8?q?=20range=20-=2090=EF=82=B0=20coverage=20-=20=20Programmable=20t?= =?UTF-8?q?imeout=20-=20=20Control=20four=20associated=20devices=20Tempera?= =?UTF-8?q?ture=20Sensor=20-=20=20-10=EF=82=B0=20-=2080=EF=82=B0C=20range?= =?UTF-8?q?=20-=20=200.1=EF=82=B0C=20resolution=20Light=20Level=20Sensor?= =?UTF-8?q?=20-=20=20Relative=20light=20level=200-100%=20Color=20Indicator?= =?UTF-8?q?=20Night=20Light=20-=20=20Eight=20colors=20Wall=20Powered=20-?= =?UTF-8?q?=20=20110VAC=2060Hz=20-=20=20No=20wires=20-=20just=20plug=20it?= =?UTF-8?q?=20in=20-=20=20Never=20needs=20batteries=20Wireless=20Z-Wave=20?= =?UTF-8?q?=C2=AE=20RF=20Technology=20-=20=20Fifth=20generation=20chipset?= =?UTF-8?q?=20-=20=20Z-Wave=20range=20extender=20-=20=20100,000=20bits/sec?= =?UTF-8?q?=20data=20rate=20-=20=20300=E2=80=99=20RF=20range=20-=20=20Over?= =?UTF-8?q?-the-air=20firmware=20update=20-=20=20Push=20button=20to=20join?= =?UTF-8?q?=20network=20-=20=20Z-Wave=20Plus=20certified=20Designed=20and?= =?UTF-8?q?=20assembled=20in=20USA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drzwave/ezmultipli.src/ezmultipli.groovy | 426 ++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy 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 +} From 80a5a41f7efdcdd9bba8d3eb3f329b4cbe458f5c Mon Sep 17 00:00:00 2001 From: jackchi Date: Tue, 11 Apr 2017 14:58:12 -0700 Subject: [PATCH 3/6] [ICP-598] Adding Z-Wave device manufacturer --- .../smartthings/dimmer-switch.src/dimmer-switch.groovy | 6 +++--- .../zwave-door-window-sensor.groovy | 3 +++ .../smartthings/zwave-switch.src/zwave-switch.groovy | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) 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/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..f5c8f8f 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" + } // 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" } From 8abb82ecaeb704da5af89b2138ae4938cfdf1185 Mon Sep 17 00:00:00 2001 From: jackchi Date: Tue, 11 Apr 2017 17:00:48 -0700 Subject: [PATCH 4/6] [ICP-598] Adding Enerwave Door/Window Sensor --- .../zwave-door-window-sensor.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f5c8f8f..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 @@ -32,7 +32,7 @@ metadata { 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 From 9b621c9a7bf27ac0d173130f58d4fc3bcfe15d51 Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Tue, 11 Apr 2017 23:35:49 -0500 Subject: [PATCH 5/6] arrival-sensor-ha: Read battery level on join In configure we were setting up the battery level to be reported every 20 seconds but we weren't reading the attribute so it would take 20 seconds before the battery level was reported. Now we read it during configure so it is updated right away after join. https://smartthings.atlassian.net/browse/DVCSMP-2568 --- .../arrival-sensor-ha.src/arrival-sensor-ha.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 +} From 7a467df659b30d6fd41cabbe3e1141ea1d7e695f Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Sun, 16 Apr 2017 14:39:32 -0600 Subject: [PATCH 6/6] DVCSMP-2572 OpenT2T: Update to 4/14 submission --- .../opent2t-smartapp-test.groovy | 313 ++++++++++++------ 1 file changed, 214 insertions(+), 99 deletions(-) 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.